diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 9a4f2b71da1ff..acfb7307f49c4 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.appbase.chromeless.md b/docs/development/core/public/kibana-plugin-public.appbase.chromeless.md
new file mode 100644
index 0000000000000..ddbf9aafbd28a
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.appbase.chromeless.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [chromeless](./kibana-plugin-public.appbase.chromeless.md)
+
+## AppBase.chromeless property
+
+Hide the UI chrome when the application is mounted. Defaults to `false`. Takes precedence over chrome service visibility settings.
+
+Signature:
+
+```typescript
+chromeless?: boolean;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.appbase.id.md b/docs/development/core/public/kibana-plugin-public.appbase.id.md
index 57daa0c94bdf6..89dd32d296104 100644
--- a/docs/development/core/public/kibana-plugin-public.appbase.id.md
+++ b/docs/development/core/public/kibana-plugin-public.appbase.id.md
@@ -4,6 +4,8 @@
## AppBase.id property
+The unique identifier of the application
+
Signature:
```typescript
diff --git a/docs/development/core/public/kibana-plugin-public.appbase.md b/docs/development/core/public/kibana-plugin-public.appbase.md
index a93a195c559b1..eb6d91cb92488 100644
--- a/docs/development/core/public/kibana-plugin-public.appbase.md
+++ b/docs/development/core/public/kibana-plugin-public.appbase.md
@@ -16,10 +16,14 @@ export interface AppBase
| Property | Type | Description |
| --- | --- | --- |
| [capabilities](./kibana-plugin-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. |
+| [chromeless](./kibana-plugin-public.appbase.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. |
| [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. |
| [icon](./kibana-plugin-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. |
-| [id](./kibana-plugin-public.appbase.id.md) | string | |
+| [id](./kibana-plugin-public.appbase.id.md) | string | The unique identifier of the application |
+| [navLinkStatus](./kibana-plugin-public.appbase.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md) |
| [order](./kibana-plugin-public.appbase.order.md) | number | An ordinal used to sort nav links relative to one another for display. |
+| [status](./kibana-plugin-public.appbase.status.md) | AppStatus | The initial status of the application. Defaulting to accessible |
| [title](./kibana-plugin-public.appbase.title.md) | string | The title of the application. |
-| [tooltip$](./kibana-plugin-public.appbase.tooltip_.md) | Observable<string> | An observable for a tooltip shown when hovering over app link. |
+| [tooltip](./kibana-plugin-public.appbase.tooltip.md) | string | A tooltip shown when hovering over app link. |
+| [updater$](./kibana-plugin-public.appbase.updater_.md) | Observable<AppUpdater> | An [AppUpdater](./kibana-plugin-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) at runtime. |
diff --git a/docs/development/core/public/kibana-plugin-public.appbase.navlinkstatus.md b/docs/development/core/public/kibana-plugin-public.appbase.navlinkstatus.md
new file mode 100644
index 0000000000000..d6744c3e75756
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.appbase.navlinkstatus.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [navLinkStatus](./kibana-plugin-public.appbase.navlinkstatus.md)
+
+## AppBase.navLinkStatus property
+
+The initial status of the application's navLink. Defaulting to `visible` if `status` is `accessible` and `hidden` if status is `inaccessible` See [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md)
+
+Signature:
+
+```typescript
+navLinkStatus?: AppNavLinkStatus;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.appbase.tooltip_.md b/docs/development/core/public/kibana-plugin-public.appbase.status.md
similarity index 56%
rename from docs/development/core/public/kibana-plugin-public.appbase.tooltip_.md
rename to docs/development/core/public/kibana-plugin-public.appbase.status.md
index 0767ead5f1455..a5fbadbeea1ff 100644
--- a/docs/development/core/public/kibana-plugin-public.appbase.tooltip_.md
+++ b/docs/development/core/public/kibana-plugin-public.appbase.status.md
@@ -1,13 +1,13 @@
-[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [tooltip$](./kibana-plugin-public.appbase.tooltip_.md)
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [status](./kibana-plugin-public.appbase.status.md)
-## AppBase.tooltip$ property
+## AppBase.status property
-An observable for a tooltip shown when hovering over app link.
+The initial status of the application. Defaulting to `accessible`
Signature:
```typescript
-tooltip$?: Observable;
+status?: AppStatus;
```
diff --git a/docs/development/core/public/kibana-plugin-public.appbase.tooltip.md b/docs/development/core/public/kibana-plugin-public.appbase.tooltip.md
new file mode 100644
index 0000000000000..85921a5a321dd
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.appbase.tooltip.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [tooltip](./kibana-plugin-public.appbase.tooltip.md)
+
+## AppBase.tooltip property
+
+A tooltip shown when hovering over app link.
+
+Signature:
+
+```typescript
+tooltip?: string;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.appbase.updater_.md b/docs/development/core/public/kibana-plugin-public.appbase.updater_.md
new file mode 100644
index 0000000000000..3edd357383449
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.appbase.updater_.md
@@ -0,0 +1,44 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [updater$](./kibana-plugin-public.appbase.updater_.md)
+
+## AppBase.updater$ property
+
+An [AppUpdater](./kibana-plugin-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) at runtime.
+
+Signature:
+
+```typescript
+updater$?: Observable;
+```
+
+## Example
+
+How to update an application navLink at runtime
+
+```ts
+// inside your plugin's setup function
+export class MyPlugin implements Plugin {
+ private appUpdater = new BehaviorSubject(() => ({}));
+
+ setup({ application }) {
+ application.register({
+ id: 'my-app',
+ title: 'My App',
+ updater$: this.appUpdater,
+ async mount(params) {
+ const { renderApp } = await import('./application');
+ return renderApp(params);
+ },
+ });
+ }
+
+ start() {
+ // later, when the navlink needs to be updated
+ appUpdater.next(() => {
+ navLinkStatus: AppNavLinkStatus.disabled,
+ })
+ }
+
+```
+
diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.md
index a63de399c2ecb..cf9bc5189af40 100644
--- a/docs/development/core/public/kibana-plugin-public.applicationsetup.md
+++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.md
@@ -16,5 +16,6 @@ export interface ApplicationSetup
| Method | Description |
| --- | --- |
| [register(app)](./kibana-plugin-public.applicationsetup.register.md) | Register an mountable application to the system. |
+| [registerAppUpdater(appUpdater$)](./kibana-plugin-public.applicationsetup.registerappupdater.md) | Register an application updater that can be used to change the [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) fields of all applications at runtime.This is meant to be used by plugins that needs to updates the whole list of applications. To only updates a specific application, use the updater$ property of the registered application instead. |
| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). |
diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registerappupdater.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.registerappupdater.md
new file mode 100644
index 0000000000000..39b4f878a3f79
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.registerappupdater.md
@@ -0,0 +1,47 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [registerAppUpdater](./kibana-plugin-public.applicationsetup.registerappupdater.md)
+
+## ApplicationSetup.registerAppUpdater() method
+
+Register an application updater that can be used to change the [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md) fields of all applications at runtime.
+
+This is meant to be used by plugins that needs to updates the whole list of applications. To only updates a specific application, use the `updater$` property of the registered application instead.
+
+Signature:
+
+```typescript
+registerAppUpdater(appUpdater$: Observable): void;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| appUpdater$ | Observable<AppUpdater> | |
+
+Returns:
+
+`void`
+
+## Example
+
+How to register an application updater that disables some applications:
+
+```ts
+// inside your plugin's setup function
+export class MyPlugin implements Plugin {
+ setup({ application }) {
+ application.registerAppUpdater(
+ new BehaviorSubject(app => {
+ if (myPluginApi.shouldDisable(app))
+ return {
+ status: AppStatus.inaccessible,
+ };
+ })
+ );
+ }
+}
+
+```
+
diff --git a/docs/development/core/public/kibana-plugin-public.appnavlinkstatus.md b/docs/development/core/public/kibana-plugin-public.appnavlinkstatus.md
new file mode 100644
index 0000000000000..d6b22ac2b9217
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.appnavlinkstatus.md
@@ -0,0 +1,23 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppNavLinkStatus](./kibana-plugin-public.appnavlinkstatus.md)
+
+## AppNavLinkStatus enum
+
+Status of the application's navLink.
+
+Signature:
+
+```typescript
+export declare enum AppNavLinkStatus
+```
+
+## Enumeration Members
+
+| Member | Value | Description |
+| --- | --- | --- |
+| default | 0 | The application navLink will be visible if the application's [AppStatus](./kibana-plugin-public.appstatus.md) is set to accessible and hidden if the application status is set to inaccessible. |
+| disabled | 2 | The application navLink is visible but inactive and not clickable in the navigation bar. |
+| hidden | 3 | The application navLink does not appear in the navigation bar. |
+| visible | 1 | The application navLink is visible and clickable in the navigation bar. |
+
diff --git a/docs/development/core/public/kibana-plugin-public.appstatus.md b/docs/development/core/public/kibana-plugin-public.appstatus.md
new file mode 100644
index 0000000000000..23fb7186569da
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.appstatus.md
@@ -0,0 +1,21 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppStatus](./kibana-plugin-public.appstatus.md)
+
+## AppStatus enum
+
+Accessibility status of an application.
+
+Signature:
+
+```typescript
+export declare enum AppStatus
+```
+
+## Enumeration Members
+
+| Member | Value | Description |
+| --- | --- | --- |
+| accessible | 0 | Application is accessible. |
+| inaccessible | 1 | Application is not accessible. |
+
diff --git a/docs/development/core/public/kibana-plugin-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-public.appupdatablefields.md
new file mode 100644
index 0000000000000..b9260c79cd972
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.appupdatablefields.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppUpdatableFields](./kibana-plugin-public.appupdatablefields.md)
+
+## AppUpdatableFields type
+
+Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-public.appupdater.md).
+
+Signature:
+
+```typescript
+export declare type AppUpdatableFields = Pick;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.appupdater.md b/docs/development/core/public/kibana-plugin-public.appupdater.md
new file mode 100644
index 0000000000000..f1b965cc2fc22
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.appupdater.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppUpdater](./kibana-plugin-public.appupdater.md)
+
+## AppUpdater type
+
+Updater for applications. see [ApplicationSetup](./kibana-plugin-public.applicationsetup.md)
+
+Signature:
+
+```typescript
+export declare type AppUpdater = (app: AppBase) => Partial | undefined;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md
index f03f3457ca93f..64cbdd880fed1 100644
--- a/docs/development/core/public/kibana-plugin-public.md
+++ b/docs/development/core/public/kibana-plugin-public.md
@@ -1,147 +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. |
-
-## 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. |
-| [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.basepath.get.md b/docs/development/core/server/kibana-plugin-server.basepath.get.md
index 6ef7022f10e62..a20bc1a4e3174 100644
--- a/docs/development/core/server/kibana-plugin-server.basepath.get.md
+++ b/docs/development/core/server/kibana-plugin-server.basepath.get.md
@@ -9,5 +9,5 @@ returns `basePath` value, specific for an incoming request.
Signature:
```typescript
-get: (request: KibanaRequest | LegacyRequest) => string;
+get: (request: LegacyRequest | KibanaRequest) => string;
```
diff --git a/docs/development/core/server/kibana-plugin-server.basepath.md b/docs/development/core/server/kibana-plugin-server.basepath.md
index 50a30f7c43fe6..63aeb7f711d97 100644
--- a/docs/development/core/server/kibana-plugin-server.basepath.md
+++ b/docs/development/core/server/kibana-plugin-server.basepath.md
@@ -20,9 +20,9 @@ The constructor for this class is marked as internal. Third-party code should no
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
-| [get](./kibana-plugin-server.basepath.get.md) | | (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest) => string | returns basePath value, specific for an incoming request. |
+| [get](./kibana-plugin-server.basepath.get.md) | | (request: LegacyRequest | KibanaRequest<unknown, unknown, unknown, any>) => string | returns basePath value, specific for an incoming request. |
| [prepend](./kibana-plugin-server.basepath.prepend.md) | | (path: string) => string | Prepends path with the basePath. |
| [remove](./kibana-plugin-server.basepath.remove.md) | | (path: string) => string | Removes the prepended basePath from the path. |
| [serverBasePath](./kibana-plugin-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-server.basepath.get.md) for getting the basePath value for a specific request |
-| [set](./kibana-plugin-server.basepath.set.md) | | (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. |
+| [set](./kibana-plugin-server.basepath.set.md) | | (request: LegacyRequest | KibanaRequest<unknown, unknown, unknown, any>, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. |
diff --git a/docs/development/core/server/kibana-plugin-server.basepath.set.md b/docs/development/core/server/kibana-plugin-server.basepath.set.md
index 56a7f644d34cc..ac08baa0bb99e 100644
--- a/docs/development/core/server/kibana-plugin-server.basepath.set.md
+++ b/docs/development/core/server/kibana-plugin-server.basepath.set.md
@@ -9,5 +9,5 @@ sets `basePath` value, specific for an incoming request.
Signature:
```typescript
-set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void;
+set: (request: LegacyRequest | KibanaRequest, requestSpecificBasePath: string) => void;
```
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 0000000000000..7ad26b85bf81c
--- /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 a38499e8f37dd..fc2f8038f973f 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 |
@@ -24,5 +25,6 @@ export interface UiSettingsParams
| [readonly](./kibana-plugin-server.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed |
| [requiresPageReload](./kibana-plugin-server.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading |
| [type](./kibana-plugin-server.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-server.uisettingstype.md) |
+| [validation](./kibana-plugin-server.uisettingsparams.validation.md) | ImageValidation | StringValidation | |
| [value](./kibana-plugin-server.uisettingsparams.value.md) | SavedObjectAttribute | default value to fall back to if a user doesn't provide any |
diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.validation.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.validation.md
new file mode 100644
index 0000000000000..f097f36e999ba
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.validation.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [validation](./kibana-plugin-server.uisettingsparams.validation.md)
+
+## UiSettingsParams.validation property
+
+Signature:
+
+```typescript
+validation?: ImageValidation | StringValidation;
+```
diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index 977a65f62202d..757c6f10f2a99 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -187,7 +187,8 @@ Refresh the page to apply the changes.
=== Search settings
[horizontal]
-`courier:batchSearches`:: When disabled, dashboard panels will load individually, and search requests will terminate when
+`courier:batchSearches`:: **Deprecated in 7.6. Starting in 8.0, this setting will be optimized internally.**
+When disabled, dashboard panels will load individually, and search requests will terminate when
users navigate away or update the query. When enabled, dashboard panels will load together when all of the data is loaded,
and searches will not terminate.
`courier:customRequestPreference`:: {ref}/search-request-body.html#request-body-search-preference[Request preference]
diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc
index 38a46a3cde5a0..8f445ff25218b 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/setup/production.asciidoc b/docs/setup/production.asciidoc
index fed4ba4886bf9..eef2b11e53d85 100644
--- a/docs/setup/production.asciidoc
+++ b/docs/setup/production.asciidoc
@@ -4,7 +4,8 @@
* <>
* <>
* <>
-* <>
+* <>
+* <>
* <>
* <>
@@ -18,7 +19,7 @@ While Kibana isn't terribly resource intensive, we still recommend running Kiban
separate from your Elasticsearch data or master nodes. To distribute Kibana
traffic across the nodes in your Elasticsearch cluster, you can run Kibana
and an Elasticsearch client node on the same machine. For more information, see
-<>.
+<>.
[float]
[[configuring-kibana-shield]]
@@ -63,7 +64,7 @@ csp.strict: true
See <>.
[float]
-[[load-balancing]]
+[[load-balancing-es]]
=== Load Balancing Across Multiple Elasticsearch Nodes
If you have multiple nodes in your Elasticsearch cluster, the easiest way to distribute Kibana requests
across the nodes is to run an Elasticsearch _Coordinating only_ node on the same machine as Kibana.
@@ -110,9 +111,40 @@ transport.tcp.port: 9300 - 9400
elasticsearch.hosts: ["http://localhost:9200"]
--------
+[float]
+[[load-balancing-kibana]]
+=== Load balancing across multiple Kibana instances
+To serve multiple Kibana installations behind a load balancer, you must change the configuration. See {kibana-ref}/settings.html[Configuring Kibana] for details on each setting.
+
+Settings unique across each Kibana instance:
+--------
+server.uuid
+server.name
+--------
+
+Settings unique across each host (for example, running multiple installations on the same virtual machine):
+--------
+logging.dest
+path.data
+pid.file
+server.port
+--------
+
+Settings that must be the same:
+--------
+xpack.security.encryptionKey //decrypting session cookies
+xpack.reporting.encryptionKey //decrypting reports stored in Elasticsearch
+--------
+
+Separate configuration files can be used from the command line by using the `-c` flag:
+--------
+bin/kibana -c config/instance1.yml
+bin/kibana -c config/instance2.yml
+--------
+
[float]
[[high-availability]]
-=== High Availability Across Multiple Elasticsearch Nodes
+=== High availability across multiple Elasticsearch nodes
Kibana can be configured to connect to multiple Elasticsearch nodes in the same cluster. In situations where a node becomes unavailable,
Kibana will transparently connect to an available node and continue operating. Requests to available hosts will be routed in a round robin fashion.
diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc
index c2ed295e83ce9..5f5d85fe8d3be 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/examples/state_containers_examples/public/todo.tsx b/examples/state_containers_examples/public/todo.tsx
index 84defb4a91e3f..84f64f99d0179 100644
--- a/examples/state_containers_examples/public/todo.tsx
+++ b/examples/state_containers_examples/public/todo.tsx
@@ -41,6 +41,7 @@ import {
PureTransition,
syncStates,
getStateFromKbnUrl,
+ BaseState,
} from '../../../src/plugins/kibana_utils/public';
import { useUrlTracker } from '../../../src/plugins/kibana_react/public';
import {
@@ -79,7 +80,7 @@ const TodoApp: React.FC = ({ filter }) => {
const { setText } = GlobalStateHelpers.useTransitions();
const { text } = GlobalStateHelpers.useState();
const { edit: editTodo, delete: deleteTodo, add: addTodo } = useTransitions();
- const todos = useState();
+ const todos = useState().todos;
const filteredTodos = todos.filter(todo => {
if (!filter) return true;
if (filter === 'completed') return todo.completed;
@@ -306,7 +307,7 @@ export const TodoAppPage: React.FC<{
);
};
-function withDefaultState(
+function withDefaultState(
stateContainer: BaseStateContainer,
// eslint-disable-next-line no-shadow
defaultState: State
@@ -314,14 +315,10 @@ function withDefaultState(
return {
...stateContainer,
set: (state: State | null) => {
- if (Array.isArray(defaultState)) {
- stateContainer.set(state || defaultState);
- } else {
- stateContainer.set({
- ...defaultState,
- ...state,
- });
- }
+ stateContainer.set({
+ ...defaultState,
+ ...state,
+ });
},
};
}
diff --git a/package.json b/package.json
index 0ed74dd65d1ab..a623b656ec9a1 100644
--- a/package.json
+++ b/package.json
@@ -118,7 +118,7 @@
"@elastic/charts": "^16.1.0",
"@elastic/datemath": "5.0.2",
"@elastic/ems-client": "1.0.5",
- "@elastic/eui": "17.3.1",
+ "@elastic/eui": "18.0.0",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "8.1.1-kibana2",
"@elastic/numeral": "2.3.3",
@@ -163,9 +163,9 @@
"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",
"deepmerge": "^4.2.2",
"del": "^5.1.0",
"elastic-apm-node": "^3.2.0",
@@ -253,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",
@@ -314,6 +315,7 @@
"@types/classnames": "^2.2.9",
"@types/d3": "^3.5.43",
"@types/dedent": "^0.7.0",
+ "@types/deep-freeze-strict": "^1.1.0",
"@types/delete-empty": "^2.0.0",
"@types/elasticsearch": "^5.0.33",
"@types/enzyme": "^3.9.0",
@@ -363,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 4c31281860767..5dbdd6e1c94e4 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 250abd162f91d..7a15c3bb742c0 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 014467d204d96..c9434f3ec1c38 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.0.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 0000000000000..d2305d643e4d2
--- /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 26c21d1674b07..d8818fe809ccb 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/renovate.json5 b/renovate.json5
index 560403046b0a5..7f67fae894110 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -210,6 +210,14 @@
'@types/dedent',
],
},
+ {
+ groupSlug: 'deep-freeze-strict',
+ groupName: 'deep-freeze-strict related packages',
+ packageNames: [
+ 'deep-freeze-strict',
+ '@types/deep-freeze-strict',
+ ],
+ },
{
groupSlug: 'delete-empty',
groupName: 'delete-empty related packages',
diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md
index b70ac610f24a7..173d73ffab664 100644
--- a/src/core/MIGRATION.md
+++ b/src/core/MIGRATION.md
@@ -55,6 +55,7 @@
- [Provide Legacy Platform API to the New platform plugin](#provide-legacy-platform-api-to-the-new-platform-plugin)
- [On the server side](#on-the-server-side)
- [On the client side](#on-the-client-side)
+ - [Updates an application navlink at runtime](#updates-an-app-navlink-at-runtime)
Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now.
@@ -1624,3 +1625,31 @@ class MyPlugin {
It's not currently possible to use a similar pattern on the client-side.
Because Legacy platform plugins heavily rely on global angular modules, which aren't available on the new platform.
So you can utilize the same approach for only *stateless Angular components*, as long as they are not consumed by a New Platform application. When New Platform applications are on the page, no legacy code is executed, so the `registerLegacyAPI` function would not be called.
+
+### Updates an application navlink at runtime
+
+The application API now provides a way to updates some of a registered application's properties after registration.
+
+```typescript
+// inside your plugin's setup function
+export class MyPlugin implements Plugin {
+ private appUpdater = new BehaviorSubject(() => ({}));
+ setup({ application }) {
+ application.register({
+ id: 'my-app',
+ title: 'My App',
+ updater$: this.appUpdater,
+ async mount(params) {
+ const { renderApp } = await import('./application');
+ return renderApp(params);
+ },
+ });
+ }
+ start() {
+ // later, when the navlink needs to be updated
+ appUpdater.next(() => {
+ navLinkStatus: AppNavLinkStatus.disabled,
+ tooltip: 'Application disabled',
+ })
+ }
+```
\ No newline at end of file
diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts
index b2e2161c92cc8..dee47315fc322 100644
--- a/src/core/public/application/application_service.mock.ts
+++ b/src/core/public/application/application_service.mock.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { Subject } from 'rxjs';
+import { BehaviorSubject, Subject } from 'rxjs';
import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
import {
@@ -25,17 +25,21 @@ import {
InternalApplicationStart,
ApplicationStart,
InternalApplicationSetup,
+ App,
+ LegacyApp,
} from './types';
import { ApplicationServiceContract } from './test_types';
const createSetupContractMock = (): jest.Mocked => ({
register: jest.fn(),
+ registerAppUpdater: jest.fn(),
registerMountContext: jest.fn(),
});
const createInternalSetupContractMock = (): jest.Mocked => ({
register: jest.fn(),
registerLegacyApp: jest.fn(),
+ registerAppUpdater: jest.fn(),
registerMountContext: jest.fn(),
});
@@ -50,8 +54,7 @@ const createInternalStartContractMock = (): jest.Mocked();
return {
- availableApps: new Map(),
- availableLegacyApps: new Map(),
+ applications$: new BehaviorSubject>(new Map()),
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
currentAppId$: currentAppId$.asObservable(),
getComponent: jest.fn(),
diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts
index 1132abc11703f..4672a42c9eb06 100644
--- a/src/core/public/application/application_service.test.ts
+++ b/src/core/public/application/application_service.test.ts
@@ -18,8 +18,8 @@
*/
import { createElement } from 'react';
-import { Subject } from 'rxjs';
-import { bufferCount, skip, takeUntil } from 'rxjs/operators';
+import { BehaviorSubject, Subject } from 'rxjs';
+import { bufferCount, skip, take, takeUntil } from 'rxjs/operators';
import { shallow } from 'enzyme';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
@@ -29,8 +29,25 @@ import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { MockCapabilitiesService, MockHistory } from './application_service.test.mocks';
import { MockLifecycle } from './test_types';
import { ApplicationService } from './application_service';
-
-function mount() {}
+import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types';
+
+const createApp = (props: Partial): App => {
+ return {
+ id: 'some-id',
+ title: 'some-title',
+ mount: () => () => undefined,
+ ...props,
+ };
+};
+
+const createLegacyApp = (props: Partial): LegacyApp => {
+ return {
+ id: 'some-id',
+ title: 'some-title',
+ appUrl: '/my-url',
+ ...props,
+ };
+};
let setupDeps: MockLifecycle<'setup'>;
let startDeps: MockLifecycle<'start'>;
@@ -53,9 +70,9 @@ describe('#setup()', () => {
it('throws an error if two apps with the same id are registered', () => {
const { register } = service.setup(setupDeps);
- register(Symbol(), { id: 'app1', mount } as any);
+ register(Symbol(), createApp({ id: 'app1' }));
expect(() =>
- register(Symbol(), { id: 'app1', mount } as any)
+ register(Symbol(), createApp({ id: 'app1' }))
).toThrowErrorMatchingInlineSnapshot(
`"An application is already registered with the id \\"app1\\""`
);
@@ -66,37 +83,91 @@ describe('#setup()', () => {
await service.start(startDeps);
expect(() =>
- register(Symbol(), { id: 'app1', mount } as any)
+ register(Symbol(), createApp({ id: 'app1' }))
).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`);
});
+ it('allows to register a statusUpdater for the application', async () => {
+ const setup = service.setup(setupDeps);
+
+ const pluginId = Symbol('plugin');
+ const updater$ = new BehaviorSubject(app => ({}));
+ setup.register(pluginId, createApp({ id: 'app1', updater$ }));
+ setup.register(pluginId, createApp({ id: 'app2' }));
+ const { applications$ } = await service.start(startDeps);
+
+ let applications = await applications$.pipe(take(1)).toPromise();
+ expect(applications.size).toEqual(2);
+ expect(applications.get('app1')).toEqual(
+ expect.objectContaining({
+ id: 'app1',
+ legacy: false,
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.accessible,
+ })
+ );
+ expect(applications.get('app2')).toEqual(
+ expect.objectContaining({
+ id: 'app2',
+ legacy: false,
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.accessible,
+ })
+ );
+
+ updater$.next(app => ({
+ status: AppStatus.inaccessible,
+ tooltip: 'App inaccessible due to reason',
+ }));
+
+ applications = await applications$.pipe(take(1)).toPromise();
+ expect(applications.size).toEqual(2);
+ expect(applications.get('app1')).toEqual(
+ expect.objectContaining({
+ id: 'app1',
+ legacy: false,
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.inaccessible,
+ tooltip: 'App inaccessible due to reason',
+ })
+ );
+ expect(applications.get('app2')).toEqual(
+ expect.objectContaining({
+ id: 'app2',
+ legacy: false,
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.accessible,
+ })
+ );
+ });
+
it('throws an error if an App with the same appRoute is registered', () => {
const { register, registerLegacyApp } = service.setup(setupDeps);
- register(Symbol(), { id: 'app1', mount } as any);
+ register(Symbol(), createApp({ id: 'app1' }));
expect(() =>
- register(Symbol(), { id: 'app2', mount, appRoute: '/app/app1' } as any)
+ register(Symbol(), createApp({ id: 'app2', appRoute: '/app/app1' }))
).toThrowErrorMatchingInlineSnapshot(
`"An application is already registered with the appRoute \\"/app/app1\\""`
);
- expect(() => registerLegacyApp({ id: 'app1' } as any)).not.toThrow();
+ expect(() => registerLegacyApp(createLegacyApp({ id: 'app1' }))).toThrow();
- register(Symbol(), { id: 'app-next', mount, appRoute: '/app/app3' } as any);
+ register(Symbol(), createApp({ id: 'app-next', appRoute: '/app/app3' }));
expect(() =>
- register(Symbol(), { id: 'app2', mount, appRoute: '/app/app3' } as any)
+ register(Symbol(), createApp({ id: 'app2', appRoute: '/app/app3' }))
).toThrowErrorMatchingInlineSnapshot(
`"An application is already registered with the appRoute \\"/app/app3\\""`
);
- expect(() => registerLegacyApp({ id: 'app3' } as any)).not.toThrow();
+ expect(() => registerLegacyApp(createLegacyApp({ id: 'app3' }))).not.toThrow();
});
it('throws an error if an App starts with the HTTP base path', () => {
const { register } = service.setup(setupDeps);
expect(() =>
- register(Symbol(), { id: 'app2', mount, appRoute: '/test/app2' } as any)
+ register(Symbol(), createApp({ id: 'app2', appRoute: '/test/app2' }))
).toThrowErrorMatchingInlineSnapshot(
`"Cannot register an application route that includes HTTP base path"`
);
@@ -107,9 +178,11 @@ describe('#setup()', () => {
it('throws an error if two apps with the same id are registered', () => {
const { registerLegacyApp } = service.setup(setupDeps);
- registerLegacyApp({ id: 'app2' } as any);
- expect(() => registerLegacyApp({ id: 'app2' } as any)).toThrowErrorMatchingInlineSnapshot(
- `"A legacy application is already registered with the id \\"app2\\""`
+ registerLegacyApp(createLegacyApp({ id: 'app2' }));
+ expect(() =>
+ registerLegacyApp(createLegacyApp({ id: 'app2' }))
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"An application is already registered with the id \\"app2\\""`
);
});
@@ -117,22 +190,228 @@ describe('#setup()', () => {
const { registerLegacyApp } = service.setup(setupDeps);
await service.start(startDeps);
- expect(() => registerLegacyApp({ id: 'app2' } as any)).toThrowErrorMatchingInlineSnapshot(
- `"Applications cannot be registered after \\"setup\\""`
- );
+ expect(() =>
+ registerLegacyApp(createLegacyApp({ id: 'app2' }))
+ ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`);
});
it('throws an error if a LegacyApp with the same appRoute is registered', () => {
const { register, registerLegacyApp } = service.setup(setupDeps);
- registerLegacyApp({ id: 'app1' } as any);
+ registerLegacyApp(createLegacyApp({ id: 'app1' }));
expect(() =>
- register(Symbol(), { id: 'app2', mount, appRoute: '/app/app1' } as any)
+ register(Symbol(), createApp({ id: 'app2', appRoute: '/app/app1' }))
).toThrowErrorMatchingInlineSnapshot(
`"An application is already registered with the appRoute \\"/app/app1\\""`
);
- expect(() => registerLegacyApp({ id: 'app1:other' } as any)).not.toThrow();
+ expect(() => registerLegacyApp(createLegacyApp({ id: 'app1:other' }))).not.toThrow();
+ });
+ });
+
+ describe('registerAppStatusUpdater', () => {
+ it('updates status fields', async () => {
+ const setup = service.setup(setupDeps);
+
+ const pluginId = Symbol('plugin');
+ setup.register(pluginId, createApp({ id: 'app1' }));
+ setup.register(pluginId, createApp({ id: 'app2' }));
+ setup.registerAppUpdater(
+ new BehaviorSubject(app => {
+ if (app.id === 'app1') {
+ return {
+ status: AppStatus.inaccessible,
+ navLinkStatus: AppNavLinkStatus.disabled,
+ tooltip: 'App inaccessible due to reason',
+ };
+ }
+ return {
+ tooltip: 'App accessible',
+ };
+ })
+ );
+ const start = await service.start(startDeps);
+ const applications = await start.applications$.pipe(take(1)).toPromise();
+
+ expect(applications.size).toEqual(2);
+ expect(applications.get('app1')).toEqual(
+ expect.objectContaining({
+ id: 'app1',
+ legacy: false,
+ navLinkStatus: AppNavLinkStatus.disabled,
+ status: AppStatus.inaccessible,
+ tooltip: 'App inaccessible due to reason',
+ })
+ );
+ expect(applications.get('app2')).toEqual(
+ expect.objectContaining({
+ id: 'app2',
+ legacy: false,
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.accessible,
+ tooltip: 'App accessible',
+ })
+ );
+ });
+
+ it(`properly combine with application's updater$`, async () => {
+ const setup = service.setup(setupDeps);
+ const pluginId = Symbol('plugin');
+ const appStatusUpdater$ = new BehaviorSubject(app => ({
+ status: AppStatus.inaccessible,
+ navLinkStatus: AppNavLinkStatus.disabled,
+ }));
+ setup.register(pluginId, createApp({ id: 'app1', updater$: appStatusUpdater$ }));
+ setup.register(pluginId, createApp({ id: 'app2' }));
+
+ setup.registerAppUpdater(
+ new BehaviorSubject(app => {
+ if (app.id === 'app1') {
+ return {
+ status: AppStatus.accessible,
+ tooltip: 'App inaccessible due to reason',
+ };
+ }
+ return {
+ status: AppStatus.inaccessible,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ };
+ })
+ );
+
+ const { applications$ } = await service.start(startDeps);
+ const applications = await applications$.pipe(take(1)).toPromise();
+
+ expect(applications.size).toEqual(2);
+ expect(applications.get('app1')).toEqual(
+ expect.objectContaining({
+ id: 'app1',
+ legacy: false,
+ navLinkStatus: AppNavLinkStatus.disabled,
+ status: AppStatus.inaccessible,
+ tooltip: 'App inaccessible due to reason',
+ })
+ );
+ expect(applications.get('app2')).toEqual(
+ expect.objectContaining({
+ id: 'app2',
+ legacy: false,
+ status: AppStatus.inaccessible,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ })
+ );
+ });
+
+ it('applies the most restrictive status in case of multiple updaters', async () => {
+ const setup = service.setup(setupDeps);
+
+ const pluginId = Symbol('plugin');
+ setup.register(pluginId, createApp({ id: 'app1' }));
+ setup.registerAppUpdater(
+ new BehaviorSubject(app => {
+ return {
+ status: AppStatus.inaccessible,
+ navLinkStatus: AppNavLinkStatus.disabled,
+ };
+ })
+ );
+ setup.registerAppUpdater(
+ new BehaviorSubject(app => {
+ return {
+ status: AppStatus.accessible,
+ navLinkStatus: AppNavLinkStatus.default,
+ };
+ })
+ );
+
+ const start = await service.start(startDeps);
+ const applications = await start.applications$.pipe(take(1)).toPromise();
+
+ expect(applications.size).toEqual(1);
+ expect(applications.get('app1')).toEqual(
+ expect.objectContaining({
+ id: 'app1',
+ legacy: false,
+ navLinkStatus: AppNavLinkStatus.disabled,
+ status: AppStatus.inaccessible,
+ })
+ );
+ });
+
+ it('emits on applications$ when a status updater changes', async () => {
+ const setup = service.setup(setupDeps);
+
+ const pluginId = Symbol('plugin');
+ setup.register(pluginId, createApp({ id: 'app1' }));
+
+ const statusUpdater = new BehaviorSubject(app => {
+ return {
+ status: AppStatus.inaccessible,
+ navLinkStatus: AppNavLinkStatus.disabled,
+ };
+ });
+ setup.registerAppUpdater(statusUpdater);
+
+ const start = await service.start(startDeps);
+ let latestValue: ReadonlyMap = new Map();
+ start.applications$.subscribe(apps => {
+ latestValue = apps;
+ });
+
+ expect(latestValue.get('app1')).toEqual(
+ expect.objectContaining({
+ id: 'app1',
+ legacy: false,
+ status: AppStatus.inaccessible,
+ navLinkStatus: AppNavLinkStatus.disabled,
+ })
+ );
+
+ statusUpdater.next(app => {
+ return {
+ status: AppStatus.accessible,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ };
+ });
+
+ expect(latestValue.get('app1')).toEqual(
+ expect.objectContaining({
+ id: 'app1',
+ legacy: false,
+ status: AppStatus.accessible,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ })
+ );
+ });
+
+ it('also updates legacy apps', async () => {
+ const setup = service.setup(setupDeps);
+
+ setup.registerLegacyApp(createLegacyApp({ id: 'app1' }));
+
+ setup.registerAppUpdater(
+ new BehaviorSubject(app => {
+ return {
+ status: AppStatus.inaccessible,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ tooltip: 'App inaccessible due to reason',
+ };
+ })
+ );
+
+ const start = await service.start(startDeps);
+ const applications = await start.applications$.pipe(take(1)).toPromise();
+
+ expect(applications.size).toEqual(1);
+ expect(applications.get('app1')).toEqual(
+ expect.objectContaining({
+ id: 'app1',
+ legacy: true,
+ status: AppStatus.inaccessible,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ tooltip: 'App inaccessible due to reason',
+ })
+ );
});
});
@@ -141,7 +420,8 @@ describe('#setup()', () => {
const container = setupDeps.context.createContextContainer.mock.results[0].value;
const pluginId = Symbol();
- registerMountContext(pluginId, 'test' as any, mount as any);
+ const mount = () => () => undefined;
+ registerMountContext(pluginId, 'test' as any, mount);
expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', mount);
});
});
@@ -171,35 +451,40 @@ describe('#start()', () => {
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
const { register, registerLegacyApp } = service.setup(setupDeps);
- register(Symbol(), { id: 'app1', mount } as any);
- registerLegacyApp({ id: 'app2' } as any);
-
- const { availableApps, availableLegacyApps } = await service.start(startDeps);
-
- expect(availableApps).toMatchInlineSnapshot(`
- Map {
- "app1" => Object {
- "appRoute": "/app/app1",
- "id": "app1",
- "mount": [Function],
- },
- }
- `);
- expect(availableLegacyApps).toMatchInlineSnapshot(`
- Map {
- "app2" => Object {
- "id": "app2",
- },
- }
- `);
+ register(Symbol(), createApp({ id: 'app1' }));
+ registerLegacyApp(createLegacyApp({ id: 'app2' }));
+
+ const { applications$ } = await service.start(startDeps);
+ const availableApps = await applications$.pipe(take(1)).toPromise();
+
+ expect(availableApps.size).toEqual(2);
+ expect([...availableApps.keys()]).toEqual(['app1', 'app2']);
+ expect(availableApps.get('app1')).toEqual(
+ expect.objectContaining({
+ appRoute: '/app/app1',
+ id: 'app1',
+ legacy: false,
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.accessible,
+ })
+ );
+ expect(availableApps.get('app2')).toEqual(
+ expect.objectContaining({
+ appUrl: '/my-url',
+ id: 'app2',
+ legacy: true,
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.accessible,
+ })
+ );
});
it('passes appIds to capabilities', async () => {
const { register } = service.setup(setupDeps);
- register(Symbol(), { id: 'app1', mount } as any);
- register(Symbol(), { id: 'app2', mount } as any);
- register(Symbol(), { id: 'app3', mount } as any);
+ register(Symbol(), createApp({ id: 'app1' }));
+ register(Symbol(), createApp({ id: 'app2' }));
+ register(Symbol(), createApp({ id: 'app3' }));
await service.start(startDeps);
expect(MockCapabilitiesService.start).toHaveBeenCalledWith({
@@ -222,29 +507,15 @@ describe('#start()', () => {
const { register, registerLegacyApp } = service.setup(setupDeps);
- register(Symbol(), { id: 'app1', mount } as any);
- registerLegacyApp({ id: 'legacyApp1' } as any);
- register(Symbol(), { id: 'app2', mount } as any);
- registerLegacyApp({ id: 'legacyApp2' } as any);
+ register(Symbol(), createApp({ id: 'app1' }));
+ registerLegacyApp(createLegacyApp({ id: 'legacyApp1' }));
+ register(Symbol(), createApp({ id: 'app2' }));
+ registerLegacyApp(createLegacyApp({ id: 'legacyApp2' }));
- const { availableApps, availableLegacyApps } = await service.start(startDeps);
+ const { applications$ } = await service.start(startDeps);
+ const availableApps = await applications$.pipe(take(1)).toPromise();
- expect(availableApps).toMatchInlineSnapshot(`
- Map {
- "app1" => Object {
- "appRoute": "/app/app1",
- "id": "app1",
- "mount": [Function],
- },
- }
- `);
- expect(availableLegacyApps).toMatchInlineSnapshot(`
- Map {
- "legacyApp1" => Object {
- "id": "legacyApp1",
- },
- }
- `);
+ expect([...availableApps.keys()]).toEqual(['app1', 'legacyApp1']);
});
describe('getComponent', () => {
@@ -290,9 +561,9 @@ describe('#start()', () => {
it('creates URL for registered appId', async () => {
const { register, registerLegacyApp } = service.setup(setupDeps);
- register(Symbol(), { id: 'app1', mount } as any);
- registerLegacyApp({ id: 'legacyApp1' } as any);
- register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any);
+ register(Symbol(), createApp({ id: 'app1' }));
+ registerLegacyApp(createLegacyApp({ id: 'legacyApp1' }));
+ register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' }));
const { getUrlForApp } = await service.start(startDeps);
@@ -329,7 +600,7 @@ describe('#start()', () => {
it('changes the browser history for custom appRoutes', async () => {
const { register } = service.setup(setupDeps);
- register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any);
+ register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' }));
const { navigateToApp } = await service.start(startDeps);
@@ -343,7 +614,7 @@ describe('#start()', () => {
it('appends a path if specified', async () => {
const { register } = service.setup(setupDeps);
- register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any);
+ register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' }));
const { navigateToApp } = await service.start(startDeps);
@@ -363,7 +634,7 @@ describe('#start()', () => {
it('includes state if specified', async () => {
const { register } = service.setup(setupDeps);
- register(Symbol(), { id: 'app2', mount, appRoute: '/custom/path' } as any);
+ register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' }));
const { navigateToApp } = await service.start(startDeps);
@@ -429,7 +700,7 @@ describe('#start()', () => {
const { registerLegacyApp } = service.setup(setupDeps);
- registerLegacyApp({ id: 'baseApp:legacyApp1' } as any);
+ registerLegacyApp(createLegacyApp({ id: 'baseApp:legacyApp1' }));
const { navigateToApp } = await service.start(startDeps);
diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx
index 5b464737ffe07..c69b96274aa95 100644
--- a/src/core/public/application/application_service.tsx
+++ b/src/core/public/application/application_service.tsx
@@ -18,8 +18,8 @@
*/
import React from 'react';
-import { BehaviorSubject, Subject } from 'rxjs';
-import { takeUntil } from 'rxjs/operators';
+import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
+import { map, takeUntil } from 'rxjs/operators';
import { createBrowserHistory, History } from 'history';
import { InjectedMetadataSetup } from '../injected_metadata';
@@ -27,18 +27,23 @@ import { HttpSetup, HttpStart } from '../http';
import { OverlayStart } from '../overlays';
import { ContextSetup, IContextContainer } from '../context';
import { AppRouter } from './ui';
-import { CapabilitiesService, Capabilities } from './capabilities';
+import { Capabilities, CapabilitiesService } from './capabilities';
import {
App,
+ AppBase,
AppLeaveHandler,
- LegacyApp,
AppMount,
AppMountDeprecated,
AppMounter,
- LegacyAppMounter,
- Mounter,
+ AppNavLinkStatus,
+ AppStatus,
+ AppUpdatableFields,
+ AppUpdater,
InternalApplicationSetup,
InternalApplicationStart,
+ LegacyApp,
+ LegacyAppMounter,
+ Mounter,
} from './types';
import { getLeaveAction, isConfirmAction } from './application_leave';
@@ -62,12 +67,13 @@ interface StartDeps {
// Mount functions with two arguments are assumed to expect deprecated `context` object.
const isAppMountDeprecated = (mount: (...args: any[]) => any): mount is AppMountDeprecated =>
mount.length === 2;
-const filterAvailable = (map: Map, capabilities: Capabilities) =>
- new Map(
- [...map].filter(
+function filterAvailable(m: Map, capabilities: Capabilities) {
+ return new Map(
+ [...m].filter(
([id]) => capabilities.navLinks[id] === undefined || capabilities.navLinks[id] === true
)
);
+}
const findMounter = (mounters: Map, appRoute?: string) =>
[...mounters].find(([, mounter]) => mounter.appRoute === appRoute);
const getAppUrl = (mounters: Map, appId: string, path: string = '') =>
@@ -75,17 +81,25 @@ const getAppUrl = (mounters: Map, appId: string, path: string =
.replace(/\/{2,}/g, '/') // Remove duplicate slashes
.replace(/\/$/, ''); // Remove trailing slash
+const allApplicationsFilter = '__ALL__';
+
+interface AppUpdaterWrapper {
+ application: string;
+ updater: AppUpdater;
+}
+
/**
* Service that is responsible for registering new applications.
* @internal
*/
export class ApplicationService {
- private readonly apps = new Map();
- private readonly legacyApps = new Map();
+ private readonly apps = new Map();
private readonly mounters = new Map();
private readonly capabilities = new CapabilitiesService();
private readonly appLeaveHandlers = new Map();
private currentAppId$ = new BehaviorSubject(undefined);
+ private readonly statusUpdaters$ = new BehaviorSubject>(new Map());
+ private readonly subscriptions: Subscription[] = [];
private stop$ = new Subject();
private registrationClosed = false;
private history?: History;
@@ -109,8 +123,22 @@ export class ApplicationService {
this.navigate = (url, state) =>
// basePath not needed here because `history` is configured with basename
this.history ? this.history.push(url, state) : redirectTo(basePath.prepend(url));
+
this.mountContext = context.createContextContainer();
+ const registerStatusUpdater = (application: string, updater$: Observable) => {
+ const updaterId = Symbol();
+ const subscription = updater$.subscribe(updater => {
+ const nextValue = new Map(this.statusUpdaters$.getValue());
+ nextValue.set(updaterId, {
+ application,
+ updater,
+ });
+ this.statusUpdaters$.next(nextValue);
+ });
+ this.subscriptions.push(subscription);
+ };
+
return {
registerMountContext: this.mountContext!.registerContext,
register: (plugin, app) => {
@@ -145,7 +173,17 @@ export class ApplicationService {
this.currentAppId$.next(app.id);
return unmount;
};
- this.apps.set(app.id, app);
+
+ const { updater$, ...appProps } = app;
+ this.apps.set(app.id, {
+ ...appProps,
+ status: app.status ?? AppStatus.accessible,
+ navLinkStatus: app.navLinkStatus ?? AppNavLinkStatus.default,
+ legacy: false,
+ });
+ if (updater$) {
+ registerStatusUpdater(app.id, updater$);
+ }
this.mounters.set(app.id, {
appRoute: app.appRoute!,
appBasePath: basePath.prepend(app.appRoute!),
@@ -158,15 +196,25 @@ export class ApplicationService {
if (this.registrationClosed) {
throw new Error('Applications cannot be registered after "setup"');
- } else if (this.legacyApps.has(app.id)) {
- throw new Error(`A legacy application is already registered with the id "${app.id}"`);
+ } else if (this.apps.has(app.id)) {
+ throw new Error(`An application is already registered with the id "${app.id}"`);
} else if (basename && appRoute!.startsWith(basename)) {
throw new Error('Cannot register an application route that includes HTTP base path');
}
const appBasePath = basePath.prepend(appRoute);
const mount: LegacyAppMounter = () => redirectTo(appBasePath);
- this.legacyApps.set(app.id, app);
+
+ const { updater$, ...appProps } = app;
+ this.apps.set(app.id, {
+ ...appProps,
+ status: app.status ?? AppStatus.accessible,
+ navLinkStatus: app.navLinkStatus ?? AppNavLinkStatus.default,
+ legacy: true,
+ });
+ if (updater$) {
+ registerStatusUpdater(app.id, updater$);
+ }
this.mounters.set(app.id, {
appRoute,
appBasePath,
@@ -174,6 +222,8 @@ export class ApplicationService {
unmountBeforeMounting: true,
});
},
+ registerAppUpdater: (appUpdater$: Observable) =>
+ registerStatusUpdater(allApplicationsFilter, appUpdater$),
};
}
@@ -190,16 +240,35 @@ export class ApplicationService {
http,
});
const availableMounters = filterAvailable(this.mounters, capabilities);
+ const availableApps = filterAvailable(this.apps, capabilities);
+
+ const applications$ = new BehaviorSubject(availableApps);
+ this.statusUpdaters$
+ .pipe(
+ map(statusUpdaters => {
+ return new Map(
+ [...availableApps].map(([id, app]) => [
+ id,
+ updateStatus(app, [...statusUpdaters.values()]),
+ ])
+ );
+ })
+ )
+ .subscribe(apps => applications$.next(apps));
return {
- availableApps: filterAvailable(this.apps, capabilities),
- availableLegacyApps: filterAvailable(this.legacyApps, capabilities),
+ applications$,
capabilities,
currentAppId$: this.currentAppId$.pipe(takeUntil(this.stop$)),
registerMountContext: this.mountContext.registerContext,
getUrlForApp: (appId, { path }: { path?: string } = {}) =>
getAppUrl(availableMounters, appId, path),
navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => {
+ const app = applications$.value.get(appId);
+ if (app && app.status !== AppStatus.accessible) {
+ // should probably redirect to the error page instead
+ throw new Error(`Trying to navigate to an inaccessible application: ${appId}`);
+ }
if (await this.shouldNavigate(overlays)) {
this.appLeaveHandlers.delete(this.currentAppId$.value!);
this.navigate!(getAppUrl(availableMounters, appId, path), state);
@@ -259,6 +328,32 @@ export class ApplicationService {
public stop() {
this.stop$.next();
this.currentAppId$.complete();
+ this.statusUpdaters$.complete();
+ this.subscriptions.forEach(sub => sub.unsubscribe());
window.removeEventListener('beforeunload', this.onBeforeUnload);
}
}
+
+const updateStatus = (app: T, statusUpdaters: AppUpdaterWrapper[]): T => {
+ let changes: Partial = {};
+ statusUpdaters.forEach(wrapper => {
+ if (wrapper.application !== allApplicationsFilter && wrapper.application !== app.id) {
+ return;
+ }
+ const fields = wrapper.updater(app);
+ if (fields) {
+ changes = {
+ ...changes,
+ ...fields,
+ // status and navLinkStatus enums are ordered by reversed priority
+ // if multiple updaters wants to change these fields, we will always follow the priority order.
+ status: Math.max(changes.status ?? 0, fields.status ?? 0),
+ navLinkStatus: Math.max(changes.navLinkStatus ?? 0, fields.navLinkStatus ?? 0),
+ };
+ }
+ });
+ return {
+ ...app,
+ ...changes,
+ };
+};
diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts
index 17fec9261accf..e7ea330657648 100644
--- a/src/core/public/application/index.ts
+++ b/src/core/public/application/index.ts
@@ -27,6 +27,10 @@ export {
AppUnmount,
AppMountContext,
AppMountParameters,
+ AppStatus,
+ AppNavLinkStatus,
+ AppUpdatableFields,
+ AppUpdater,
ApplicationSetup,
ApplicationStart,
AppLeaveHandler,
diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts
index 4caf236979c37..0d955482d2226 100644
--- a/src/core/public/application/types.ts
+++ b/src/core/public/application/types.ts
@@ -34,6 +34,9 @@ import { SavedObjectsStart } from '../saved_objects';
/** @public */
export interface AppBase {
+ /**
+ * The unique identifier of the application
+ */
id: string;
/**
@@ -41,15 +44,62 @@ export interface AppBase {
*/
title: string;
+ /**
+ * The initial status of the application.
+ * Defaulting to `accessible`
+ */
+ status?: AppStatus;
+
+ /**
+ * The initial status of the application's navLink.
+ * Defaulting to `visible` if `status` is `accessible` and `hidden` if status is `inaccessible`
+ * See {@link AppNavLinkStatus}
+ */
+ navLinkStatus?: AppNavLinkStatus;
+
+ /**
+ * An {@link AppUpdater} observable that can be used to update the application {@link AppUpdatableFields} at runtime.
+ *
+ * @example
+ *
+ * How to update an application navLink at runtime
+ *
+ * ```ts
+ * // inside your plugin's setup function
+ * export class MyPlugin implements Plugin {
+ * private appUpdater = new BehaviorSubject(() => ({}));
+ *
+ * setup({ application }) {
+ * application.register({
+ * id: 'my-app',
+ * title: 'My App',
+ * updater$: this.appUpdater,
+ * async mount(params) {
+ * const { renderApp } = await import('./application');
+ * return renderApp(params);
+ * },
+ * });
+ * }
+ *
+ * start() {
+ * // later, when the navlink needs to be updated
+ * appUpdater.next(() => {
+ * navLinkStatus: AppNavLinkStatus.disabled,
+ * })
+ * }
+ * ```
+ */
+ updater$?: Observable;
+
/**
* An ordinal used to sort nav links relative to one another for display.
*/
order?: number;
/**
- * An observable for a tooltip shown when hovering over app link.
+ * A tooltip shown when hovering over app link.
*/
- tooltip$?: Observable;
+ tooltip?: string;
/**
* A EUI iconType that will be used for the app's icon. This icon
@@ -67,8 +117,76 @@ export interface AppBase {
* Custom capabilities defined by the app.
*/
capabilities?: Partial;
+
+ /**
+ * Flag to keep track of legacy applications.
+ * For internal use only. any value will be overridden when registering an App.
+ *
+ * @internal
+ */
+ legacy?: boolean;
+
+ /**
+ * Hide the UI chrome when the application is mounted. Defaults to `false`.
+ * Takes precedence over chrome service visibility settings.
+ */
+ chromeless?: boolean;
}
+/**
+ * Accessibility status of an application.
+ *
+ * @public
+ */
+export enum AppStatus {
+ /**
+ * Application is accessible.
+ */
+ accessible = 0,
+ /**
+ * Application is not accessible.
+ */
+ inaccessible = 1,
+}
+
+/**
+ * Status of the application's navLink.
+ *
+ * @public
+ */
+export enum AppNavLinkStatus {
+ /**
+ * The application navLink will be `visible` if the application's {@link AppStatus} is set to `accessible`
+ * and `hidden` if the application status is set to `inaccessible`.
+ */
+ default = 0,
+ /**
+ * The application navLink is visible and clickable in the navigation bar.
+ */
+ visible = 1,
+ /**
+ * The application navLink is visible but inactive and not clickable in the navigation bar.
+ */
+ disabled = 2,
+ /**
+ * The application navLink does not appear in the navigation bar.
+ */
+ hidden = 3,
+}
+
+/**
+ * Defines the list of fields that can be updated via an {@link AppUpdater}.
+ * @public
+ */
+export type AppUpdatableFields = Pick;
+
+/**
+ * Updater for applications.
+ * see {@link ApplicationSetup}
+ * @public
+ */
+export type AppUpdater = (app: AppBase) => Partial | undefined;
+
/**
* Extension of {@link AppBase | common app properties} with the mount function.
* @public
@@ -374,6 +492,35 @@ export interface ApplicationSetup {
*/
register(app: App): void;
+ /**
+ * Register an application updater that can be used to change the {@link AppUpdatableFields} fields
+ * of all applications at runtime.
+ *
+ * This is meant to be used by plugins that needs to updates the whole list of applications.
+ * To only updates a specific application, use the `updater$` property of the registered application instead.
+ *
+ * @example
+ *
+ * How to register an application updater that disables some applications:
+ *
+ * ```ts
+ * // inside your plugin's setup function
+ * export class MyPlugin implements Plugin {
+ * setup({ application }) {
+ * application.registerAppUpdater(
+ * new BehaviorSubject(app => {
+ * if (myPluginApi.shouldDisable(app))
+ * return {
+ * status: AppStatus.inaccessible,
+ * };
+ * })
+ * );
+ * }
+ * }
+ * ```
+ */
+ registerAppUpdater(appUpdater$: Observable): void;
+
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}.
@@ -389,7 +536,7 @@ export interface ApplicationSetup {
}
/** @internal */
-export interface InternalApplicationSetup {
+export interface InternalApplicationSetup extends Pick {
/**
* Register an mountable application to the system.
* @param plugin - opaque ID of the plugin that registers this application
@@ -462,16 +609,11 @@ export interface ApplicationStart {
export interface InternalApplicationStart
extends Pick {
/**
- * Apps available based on the current capabilities. Should be used
- * to show navigation links and make routing decisions.
- */
- availableApps: ReadonlyMap;
- /**
- * Apps available based on the current capabilities. Should be used
- * to show navigation links and make routing decisions.
- * @internal
+ * Apps available based on the current capabilities.
+ * Should be used to show navigation links and make routing decisions.
+ * Applications manually disabled from the client-side using {@link AppUpdater}
*/
- availableLegacyApps: ReadonlyMap;
+ applications$: Observable>;
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts
index d9c35b20db03b..abd04722a49f2 100644
--- a/src/core/public/chrome/chrome_service.test.ts
+++ b/src/core/public/chrome/chrome_service.test.ts
@@ -18,7 +18,7 @@
*/
import * as Rx from 'rxjs';
-import { toArray } from 'rxjs/operators';
+import { take, toArray } from 'rxjs/operators';
import { shallow } from 'enzyme';
import React from 'react';
@@ -54,7 +54,9 @@ function defaultStartDeps(availableApps?: App[]) {
};
if (availableApps) {
- deps.application.availableApps = new Map(availableApps.map(app => [app.id, app]));
+ deps.application.applications$ = new Rx.BehaviorSubject>(
+ new Map(availableApps.map(app => [app.id, app]))
+ );
}
return deps;
@@ -211,13 +213,14 @@ describe('start', () => {
new FakeApp('beta', true),
new FakeApp('gamma', false),
]);
- const { availableApps, navigateToApp } = startDeps.application;
+ const { applications$, navigateToApp } = startDeps.application;
const { chrome, service } = await start({ startDeps });
const promise = chrome
.getIsVisible$()
.pipe(toArray())
.toPromise();
+ const availableApps = await applications$.pipe(take(1)).toPromise();
[...availableApps.keys()].forEach(appId => navigateToApp(appId));
service.stop();
diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx
index 18c0c9870d72f..09ea1afe35766 100644
--- a/src/core/public/chrome/chrome_service.tsx
+++ b/src/core/public/chrome/chrome_service.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { BehaviorSubject, Observable, ReplaySubject, combineLatest, of, merge } from 'rxjs';
-import { map, takeUntil } from 'rxjs/operators';
+import { flatMap, map, takeUntil } from 'rxjs/operators';
import { parse } from 'url';
import { i18n } from '@kbn/i18n';
@@ -118,15 +118,16 @@ export class ChromeService {
// combineLatest below regardless of having an application value yet.
of(isEmbedded),
application.currentAppId$.pipe(
- map(
- appId =>
- !!appId &&
- application.availableApps.has(appId) &&
- !!application.availableApps.get(appId)!.chromeless
+ flatMap(appId =>
+ application.applications$.pipe(
+ map(applications => {
+ return !!appId && applications.has(appId) && !!applications.get(appId)!.chromeless;
+ })
+ )
)
)
);
- 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/nav_links/nav_links_service.test.ts b/src/core/public/chrome/nav_links/nav_links_service.test.ts
index 5a45491df28e7..3d9a4bfdb6a56 100644
--- a/src/core/public/chrome/nav_links/nav_links_service.test.ts
+++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts
@@ -20,34 +20,47 @@
import { NavLinksService } from './nav_links_service';
import { take, map, takeLast } from 'rxjs/operators';
import { App, LegacyApp } from '../../application';
+import { BehaviorSubject } from 'rxjs';
-const mockAppService = {
- availableApps: new Map(
- ([
- { id: 'app1', order: 0, title: 'App 1', icon: 'app1' },
- {
- id: 'app2',
- order: -10,
- title: 'App 2',
- euiIconType: 'canvasApp',
- },
- { id: 'chromelessApp', order: 20, title: 'Chromless App', chromeless: true },
- ] as App[]).map(app => [app.id, app])
- ),
- availableLegacyApps: new Map(
- ([
- { id: 'legacyApp1', order: 5, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' },
- {
- id: 'legacyApp2',
- order: -5,
- title: 'Legacy App 2',
- euiIconType: 'canvasApp',
- appUrl: '/app2',
- },
- { id: 'legacyApp3', order: 15, title: 'Legacy App 3', appUrl: '/app3' },
- ] as LegacyApp[]).map(app => [app.id, app])
- ),
-} as any;
+const availableApps = new Map([
+ ['app1', { id: 'app1', order: 0, title: 'App 1', icon: 'app1' }],
+ [
+ 'app2',
+ {
+ id: 'app2',
+ order: -10,
+ title: 'App 2',
+ euiIconType: 'canvasApp',
+ },
+ ],
+ ['chromelessApp', { id: 'chromelessApp', order: 20, title: 'Chromless App', chromeless: true }],
+ [
+ 'legacyApp1',
+ {
+ id: 'legacyApp1',
+ order: 5,
+ title: 'Legacy App 1',
+ icon: 'legacyApp1',
+ appUrl: '/app1',
+ legacy: true,
+ },
+ ],
+ [
+ 'legacyApp2',
+ {
+ id: 'legacyApp2',
+ order: -10,
+ title: 'Legacy App 2',
+ euiIconType: 'canvasApp',
+ appUrl: '/app2',
+ legacy: true,
+ },
+ ],
+ [
+ 'legacyApp3',
+ { id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3', legacy: true },
+ ],
+]);
const mockHttp = {
basePath: {
@@ -57,10 +70,16 @@ const mockHttp = {
describe('NavLinksService', () => {
let service: NavLinksService;
+ let mockAppService: any;
let start: ReturnType;
beforeEach(() => {
service = new NavLinksService();
+ mockAppService = {
+ applications$: new BehaviorSubject>(
+ availableApps as any
+ ),
+ };
start = service.start({ application: mockAppService, http: mockHttp });
});
@@ -183,22 +202,36 @@ describe('NavLinksService', () => {
.toPromise()
).toEqual(['legacyApp1']);
});
+
+ it('still removes all other links when availableApps are re-emitted', async () => {
+ start.showOnly('legacyApp2');
+ mockAppService.applications$.next(mockAppService.applications$.value);
+ expect(
+ await start
+ .getNavLinks$()
+ .pipe(
+ take(1),
+ map(links => links.map(l => l.id))
+ )
+ .toPromise()
+ ).toEqual(['legacyApp2']);
+ });
});
describe('#update()', () => {
it('updates the navlinks and returns the updated link', async () => {
- expect(start.update('legacyApp1', { hidden: true })).toMatchInlineSnapshot(`
- Object {
- "appUrl": "/app1",
- "baseUrl": "http://localhost/wow/app1",
- "hidden": true,
- "icon": "legacyApp1",
- "id": "legacyApp1",
- "legacy": true,
- "order": 5,
- "title": "Legacy App 1",
- }
- `);
+ expect(start.update('legacyApp1', { hidden: true })).toEqual(
+ expect.objectContaining({
+ appUrl: '/app1',
+ disabled: false,
+ hidden: true,
+ icon: 'legacyApp1',
+ id: 'legacyApp1',
+ legacy: true,
+ order: 5,
+ title: 'Legacy App 1',
+ })
+ );
const hiddenLinkIds = await start
.getNavLinks$()
.pipe(
@@ -212,6 +245,19 @@ describe('NavLinksService', () => {
it('returns undefined if link does not exist', () => {
expect(start.update('fake', { hidden: true })).toBeUndefined();
});
+
+ it('keeps the updated link when availableApps are re-emitted', async () => {
+ start.update('legacyApp1', { hidden: true });
+ mockAppService.applications$.next(mockAppService.applications$.value);
+ const hiddenLinkIds = await start
+ .getNavLinks$()
+ .pipe(
+ take(1),
+ map(links => links.filter(l => l.hidden).map(l => l.id))
+ )
+ .toPromise();
+ expect(hiddenLinkIds).toEqual(['legacyApp1']);
+ });
});
describe('#enableForcedAppSwitcherNavigation()', () => {
diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts
index 31a729f90cd93..650ef77b6fe42 100644
--- a/src/core/public/chrome/nav_links/nav_links_service.ts
+++ b/src/core/public/chrome/nav_links/nav_links_service.ts
@@ -18,11 +18,13 @@
*/
import { sortBy } from 'lodash';
-import { BehaviorSubject, ReplaySubject, Observable } from 'rxjs';
+import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
-import { NavLinkWrapper, ChromeNavLinkUpdateableFields, ChromeNavLink } from './nav_link';
+
import { InternalApplicationStart } from '../../application';
import { HttpStart } from '../../http';
+import { ChromeNavLink, ChromeNavLinkUpdateableFields, NavLinkWrapper } from './nav_link';
+import { toNavLink } from './to_nav_link';
interface StartDeps {
application: InternalApplicationStart;
@@ -95,39 +97,38 @@ export interface ChromeNavLinks {
getForceAppSwitcherNavigation$(): Observable;
}
+type LinksUpdater = (navLinks: Map) => Map;
+
export class NavLinksService {
private readonly stop$ = new ReplaySubject(1);
public start({ application, http }: StartDeps): ChromeNavLinks {
- const appLinks = [...application.availableApps]
- .filter(([, app]) => !app.chromeless)
- .map(
- ([appId, app]) =>
- [
- appId,
- new NavLinkWrapper({
- ...app,
- legacy: false,
- baseUrl: relativeToAbsolute(http.basePath.prepend(`/app/${appId}`)),
- }),
- ] as [string, NavLinkWrapper]
- );
-
- const legacyAppLinks = [...application.availableLegacyApps].map(
- ([appId, app]) =>
- [
- appId,
- new NavLinkWrapper({
- ...app,
- legacy: true,
- baseUrl: relativeToAbsolute(http.basePath.prepend(app.appUrl)),
- }),
- ] as [string, NavLinkWrapper]
+ const appLinks$ = application.applications$.pipe(
+ map(apps => {
+ return new Map(
+ [...apps]
+ .filter(([, app]) => !app.chromeless)
+ .map(([appId, app]) => [appId, toNavLink(app, http.basePath)])
+ );
+ })
);
- const navLinks$ = new BehaviorSubject>(
- new Map([...legacyAppLinks, ...appLinks])
- );
+ // now that availableApps$ is an observable, we need to keep record of all
+ // manual link modifications to be able to re-apply then after every
+ // availableApps$ changes.
+ const linkUpdaters$ = new BehaviorSubject([]);
+ const navLinks$ = new BehaviorSubject>(new Map());
+
+ combineLatest([appLinks$, linkUpdaters$])
+ .pipe(
+ map(([appLinks, linkUpdaters]) => {
+ return linkUpdaters.reduce((links, updater) => updater(links), appLinks);
+ })
+ )
+ .subscribe(navlinks => {
+ navLinks$.next(navlinks);
+ });
+
const forceAppSwitcherNavigation$ = new BehaviorSubject(false);
return {
@@ -153,7 +154,10 @@ export class NavLinksService {
return;
}
- navLinks$.next(new Map([...navLinks$.value.entries()].filter(([linkId]) => linkId === id)));
+ const updater: LinksUpdater = navLinks =>
+ new Map([...navLinks.entries()].filter(([linkId]) => linkId === id));
+
+ linkUpdaters$.next([...linkUpdaters$.value, updater]);
},
update(id: string, values: ChromeNavLinkUpdateableFields) {
@@ -161,17 +165,17 @@ export class NavLinksService {
return;
}
- navLinks$.next(
+ const updater: LinksUpdater = navLinks =>
new Map(
- [...navLinks$.value.entries()].map(([linkId, link]) => {
+ [...navLinks.entries()].map(([linkId, link]) => {
return [linkId, link.id === id ? link.update(values) : link] as [
string,
NavLinkWrapper
];
})
- )
- );
+ );
+ linkUpdaters$.next([...linkUpdaters$.value, updater]);
return this.get(id);
},
@@ -196,10 +200,3 @@ function sortNavLinks(navLinks: ReadonlyMap) {
'order'
);
}
-
-function relativeToAbsolute(url: string) {
- // convert all link urls to absolute urls
- const a = document.createElement('a');
- a.setAttribute('href', url);
- return a.href;
-}
diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts
new file mode 100644
index 0000000000000..23fdabe0f3430
--- /dev/null
+++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts
@@ -0,0 +1,178 @@
+/*
+ * 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 { App, AppMount, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application';
+import { toNavLink } from './to_nav_link';
+
+import { httpServiceMock } from '../../mocks';
+
+function mount() {}
+
+const app = (props: Partial = {}): App => ({
+ mount: (mount as unknown) as AppMount,
+ id: 'some-id',
+ title: 'some-title',
+ status: AppStatus.accessible,
+ navLinkStatus: AppNavLinkStatus.default,
+ appRoute: `/app/some-id`,
+ legacy: false,
+ ...props,
+});
+
+const legacyApp = (props: Partial = {}): LegacyApp => ({
+ appUrl: '/my-app-url',
+ id: 'some-id',
+ title: 'some-title',
+ status: AppStatus.accessible,
+ navLinkStatus: AppNavLinkStatus.default,
+ legacy: true,
+ ...props,
+});
+
+describe('toNavLink', () => {
+ const basePath = httpServiceMock.createSetupContract({ basePath: '/base-path' }).basePath;
+
+ it('uses the application properties when creating the navLink', () => {
+ const link = toNavLink(
+ app({
+ id: 'id',
+ title: 'title',
+ order: 12,
+ tooltip: 'tooltip',
+ euiIconType: 'my-icon',
+ }),
+ basePath
+ );
+ expect(link.properties).toEqual(
+ expect.objectContaining({
+ id: 'id',
+ title: 'title',
+ order: 12,
+ tooltip: 'tooltip',
+ euiIconType: 'my-icon',
+ })
+ );
+ });
+
+ it('flags legacy apps when converting to navLink', () => {
+ expect(toNavLink(app({}), basePath).properties.legacy).toEqual(false);
+ expect(toNavLink(legacyApp({}), basePath).properties.legacy).toEqual(true);
+ });
+
+ it('handles applications with custom app route', () => {
+ const link = toNavLink(
+ app({
+ appRoute: '/my-route/my-path',
+ }),
+ basePath
+ );
+ expect(link.properties.baseUrl).toEqual('http://localhost/base-path/my-route/my-path');
+ });
+
+ it('uses appUrl when converting legacy applications', () => {
+ expect(
+ toNavLink(
+ legacyApp({
+ appUrl: '/my-legacy-app/#foo',
+ }),
+ basePath
+ ).properties
+ ).toEqual(
+ expect.objectContaining({
+ baseUrl: 'http://localhost/base-path/my-legacy-app/#foo',
+ })
+ );
+ });
+
+ it('uses the application status when the navLinkStatus is set to default', () => {
+ expect(
+ toNavLink(
+ app({
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.accessible,
+ }),
+ basePath
+ ).properties
+ ).toEqual(
+ expect.objectContaining({
+ disabled: false,
+ hidden: false,
+ })
+ );
+
+ expect(
+ toNavLink(
+ app({
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.inaccessible,
+ }),
+ basePath
+ ).properties
+ ).toEqual(
+ expect.objectContaining({
+ disabled: false,
+ hidden: true,
+ })
+ );
+ });
+
+ it('uses the navLinkStatus of the application to set the hidden and disabled properties', () => {
+ expect(
+ toNavLink(
+ app({
+ navLinkStatus: AppNavLinkStatus.visible,
+ }),
+ basePath
+ ).properties
+ ).toEqual(
+ expect.objectContaining({
+ disabled: false,
+ hidden: false,
+ })
+ );
+
+ expect(
+ toNavLink(
+ app({
+ navLinkStatus: AppNavLinkStatus.hidden,
+ }),
+ basePath
+ ).properties
+ ).toEqual(
+ expect.objectContaining({
+ disabled: false,
+ hidden: true,
+ })
+ );
+
+ expect(
+ toNavLink(
+ app({
+ navLinkStatus: AppNavLinkStatus.disabled,
+ }),
+ basePath
+ ).properties
+ ).toEqual(
+ expect.objectContaining({
+ disabled: true,
+ hidden: false,
+ })
+ );
+ });
+});
diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts
new file mode 100644
index 0000000000000..18e4b7b26b6ba
--- /dev/null
+++ b/src/core/public/chrome/nav_links/to_nav_link.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.
+ */
+
+import { App, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application';
+import { IBasePath } from '../../http';
+import { NavLinkWrapper } from './nav_link';
+
+export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper {
+ const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default;
+ return new NavLinkWrapper({
+ ...app,
+ hidden: useAppStatus
+ ? app.status === AppStatus.inaccessible
+ : app.navLinkStatus === AppNavLinkStatus.hidden,
+ disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled,
+ legacy: isLegacyApp(app),
+ baseUrl: isLegacyApp(app)
+ ? relativeToAbsolute(basePath.prepend(app.appUrl))
+ : relativeToAbsolute(basePath.prepend(app.appRoute!)),
+ });
+}
+
+function relativeToAbsolute(url: string) {
+ // convert all link urls to absolute urls
+ const a = document.createElement('a');
+ a.setAttribute('href', url);
+ return a.href;
+}
+
+function isLegacyApp(app: App | LegacyApp): app is LegacyApp {
+ return app.legacy === true;
+}
diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx
index 75f78ac8b2fa0..d05a6bb53405c 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)
),
@@ -309,7 +307,7 @@ class HeaderUI extends Component {
.filter(navLink => !navLink.hidden)
.map(navLink => ({
key: navLink.id,
- label: navLink.title,
+ label: navLink.tooltip ?? navLink.title,
// Use href and onClick to support "open in new tab" and SPA navigation in the same link
href: navLink.href,
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 44dc76bfe6e32..36b220f16f395 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -115,6 +115,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`,
+ },
},
});
}
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index ea704749c6131..5b17eccc37f8b 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -94,6 +94,10 @@ export {
AppLeaveAction,
AppLeaveDefaultAction,
AppLeaveConfirmAction,
+ AppStatus,
+ AppNavLinkStatus,
+ AppUpdatableFields,
+ AppUpdater,
} from './application';
export {
diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts
index a4fdd86de5311..f906aff1759e2 100644
--- a/src/core/public/legacy/legacy_service.ts
+++ b/src/core/public/legacy/legacy_service.ts
@@ -81,6 +81,7 @@ export class LegacyPlatformService {
...core,
getStartServices: () => this.startDependencies,
application: {
+ ...core.application,
register: notSupported(`core.application.register()`),
registerMountContext: notSupported(`core.application.registerMountContext()`),
},
diff --git a/src/core/public/notifications/toasts/global_toast_list.test.tsx b/src/core/public/notifications/toasts/global_toast_list.test.tsx
index 61d73ac233188..dc2a9dabe791e 100644
--- a/src/core/public/notifications/toasts/global_toast_list.test.tsx
+++ b/src/core/public/notifications/toasts/global_toast_list.test.tsx
@@ -57,9 +57,9 @@ it('subscribes to toasts$ on mount and unsubscribes on unmount', () => {
it('passes latest value from toasts$ to ', () => {
const el = shallow(
render({
- toasts$: Rx.from([[], [{ id: 1 }], [{ id: 1 }, { id: 2 }]]) as any,
+ toasts$: Rx.from([[], [{ id: '1' }], [{ id: '1' }, { id: '2' }]]) as any,
})
);
- expect(el.find(EuiGlobalToastList).prop('toasts')).toEqual([{ id: 1 }, { id: 2 }]);
+ expect(el.find(EuiGlobalToastList).prop('toasts')).toEqual([{ id: '1' }, { id: '2' }]);
});
diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts
index 848f46605d4de..f146c2452868b 100644
--- a/src/core/public/plugins/plugin_context.ts
+++ b/src/core/public/plugins/plugin_context.ts
@@ -96,6 +96,7 @@ export function createPluginSetupContext<
return {
application: {
register: app => deps.application.register(plugin.opaqueId, app),
+ registerAppUpdater: statusUpdater$ => deps.application.registerAppUpdater(statusUpdater$),
registerMountContext: (contextName, provider) =>
deps.application.registerMountContext(plugin.opaqueId, contextName, provider),
},
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index c76d6191de8a3..aef689162f45a 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -26,13 +26,18 @@ export interface App extends AppBase {
// @public (undocumented)
export interface AppBase {
capabilities?: Partial;
+ chromeless?: boolean;
euiIconType?: string;
icon?: string;
- // (undocumented)
id: string;
+ // @internal
+ legacy?: boolean;
+ navLinkStatus?: AppNavLinkStatus;
order?: number;
+ status?: AppStatus;
title: string;
- tooltip$?: Observable;
+ tooltip?: string;
+ updater$?: Observable;
}
// @public
@@ -74,6 +79,7 @@ export type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction
// @public (undocumented)
export interface ApplicationSetup {
register(app: App): void;
+ registerAppUpdater(appUpdater$: Observable): void;
// @deprecated
registerMountContext(contextName: T, provider: IContextProvider): void;
}
@@ -123,9 +129,29 @@ export interface AppMountParameters {
onAppLeave: (handler: AppLeaveHandler) => void;
}
+// @public
+export enum AppNavLinkStatus {
+ default = 0,
+ disabled = 2,
+ hidden = 3,
+ visible = 1
+}
+
+// @public
+export enum AppStatus {
+ accessible = 0,
+ inaccessible = 1
+}
+
// @public
export type AppUnmount = () => void;
+// @public
+export type AppUpdatableFields = Pick;
+
+// @public
+export type AppUpdater = (app: AppBase) => Partial | undefined;
+
// @public
export interface Capabilities {
[key: string]: Record>;
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 0000000000000..746e37b1214d9
--- /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 0000000000000..72faaeac588be
--- /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 ed835574a32f9..437a602a3d447 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 7a747faa2673f..58b8c1921e333 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/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts
index 1b52f22c4da09..a4e51ca55b3e7 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts
@@ -74,8 +74,6 @@ const createInternalSetupContractMock = () => {
legacy: {
config$: new BehaviorSubject({} as ElasticsearchConfig),
},
- adminClient$: new BehaviorSubject(createClusterClientMock()),
- dataClient$: new BehaviorSubject(createClusterClientMock()),
};
setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock());
setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock());
diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts
index 9f694ac1c46da..5a7d223fec7ad 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts
@@ -21,7 +21,7 @@ import { first } from 'rxjs/operators';
import { MockClusterClient } from './elasticsearch_service.test.mocks';
-import { BehaviorSubject, combineLatest } from 'rxjs';
+import { BehaviorSubject } from 'rxjs';
import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
@@ -91,44 +91,6 @@ describe('#setup', () => {
expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1);
});
- it('returns data and admin client observables as a part of the contract', async () => {
- const mockAdminClusterClientInstance = { close: jest.fn() };
- const mockDataClusterClientInstance = { close: jest.fn() };
- MockClusterClient.mockImplementationOnce(
- () => mockAdminClusterClientInstance
- ).mockImplementationOnce(() => mockDataClusterClientInstance);
-
- const setupContract = await elasticsearchService.setup(deps);
-
- const [esConfig, adminClient, dataClient] = await combineLatest(
- setupContract.legacy.config$,
- setupContract.adminClient$,
- setupContract.dataClient$
- )
- .pipe(first())
- .toPromise();
-
- expect(adminClient).toBe(mockAdminClusterClientInstance);
- expect(dataClient).toBe(mockDataClusterClientInstance);
-
- expect(MockClusterClient).toHaveBeenCalledTimes(2);
- expect(MockClusterClient).toHaveBeenNthCalledWith(
- 1,
- esConfig,
- expect.objectContaining({ context: ['elasticsearch', 'admin'] }),
- undefined
- );
- expect(MockClusterClient).toHaveBeenNthCalledWith(
- 2,
- esConfig,
- expect.objectContaining({ context: ['elasticsearch', 'data'] }),
- expect.any(Function)
- );
-
- expect(mockAdminClusterClientInstance.close).not.toHaveBeenCalled();
- expect(mockDataClusterClientInstance.close).not.toHaveBeenCalled();
- });
-
describe('#createClient', () => {
it('allows to specify config properties', async () => {
const setupContract = await elasticsearchService.setup(deps);
diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts
index db3fda3a504ab..aba246ce66fb5 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.ts
@@ -152,8 +152,6 @@ export class ElasticsearchService implements CoreService
clients.config)) },
- adminClient$,
- dataClient$,
adminClient,
dataClient,
diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts
index 22340bf3f2fc6..899b273c5c60a 100644
--- a/src/core/server/elasticsearch/types.ts
+++ b/src/core/server/elasticsearch/types.ts
@@ -77,7 +77,4 @@ export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceS
readonly legacy: {
readonly config$: Observable;
};
-
- readonly adminClient$: Observable;
- readonly dataClient$: Observable;
}
diff --git a/src/core/server/http/integration_tests/core_service.test.mocks.ts b/src/core/server/http/integration_tests/core_service.test.mocks.ts
index 3982df567ed7c..6fa3357168027 100644
--- a/src/core/server/http/integration_tests/core_service.test.mocks.ts
+++ b/src/core/server/http/integration_tests/core_service.test.mocks.ts
@@ -16,8 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock';
export const clusterClientMock = jest.fn();
jest.doMock('../../elasticsearch/scoped_cluster_client', () => ({
- ScopedClusterClient: clusterClientMock,
+ ScopedClusterClient: clusterClientMock.mockImplementation(function() {
+ return elasticsearchServiceMock.createScopedClusterClient();
+ }),
}));
diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts
index f3867faa2ae75..65c4f1432721d 100644
--- a/src/core/server/http/integration_tests/core_services.test.ts
+++ b/src/core/server/http/integration_tests/core_services.test.ts
@@ -133,7 +133,7 @@ describe('http service', () => {
const { http } = await root.setup();
const { registerAuth } = http;
- await registerAuth((req, res, toolkit) => {
+ registerAuth((req, res, toolkit) => {
return toolkit.authenticated({ responseHeaders: authResponseHeader });
});
@@ -157,7 +157,7 @@ describe('http service', () => {
const { http } = await root.setup();
const { registerAuth } = http;
- await registerAuth((req, res, toolkit) => {
+ registerAuth((req, res, toolkit) => {
return toolkit.authenticated({ responseHeaders: authResponseHeader });
});
@@ -222,12 +222,15 @@ describe('http service', () => {
const { http } = await root.setup();
const { registerAuth, createRouter } = http;
- await registerAuth((req, res, toolkit) =>
- toolkit.authenticated({ requestHeaders: authHeaders })
- );
+ registerAuth((req, res, toolkit) => toolkit.authenticated({ requestHeaders: authHeaders }));
const router = createRouter('/new-platform');
- router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
+ router.get({ path: '/', validate: false }, async (context, req, res) => {
+ // it forces client initialization since the core creates them lazily.
+ await context.core.elasticsearch.adminClient.callAsCurrentUser('ping');
+ await context.core.elasticsearch.dataClient.callAsCurrentUser('ping');
+ return res.ok();
+ });
await root.start();
@@ -247,7 +250,12 @@ describe('http service', () => {
const { createRouter } = http;
const router = createRouter('/new-platform');
- router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
+ router.get({ path: '/', validate: false }, async (context, req, res) => {
+ // it forces client initialization since the core creates them lazily.
+ await context.core.elasticsearch.adminClient.callAsCurrentUser('ping');
+ await context.core.elasticsearch.dataClient.callAsCurrentUser('ping');
+ return res.ok();
+ });
await root.start();
diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts
index 608392e4943f9..af4db68ee95e1 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 cc36b90ec526d..7a03cefc38c1a 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 d2e7a39236d0a..9867274d224bd 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/mocks.ts b/src/core/server/mocks.ts
index 073d380d3aa67..c7082d46313ae 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -37,6 +37,7 @@ export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.
export { httpServiceMock } from './http/http_service.mock';
export { loggingServiceMock } from './logging/logging_service.mock';
export { savedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock';
+export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock';
export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
import { uuidServiceMock } from './uuid/uuid_service.mock';
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index bf7dc14c73265..7f3a960571012 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;
@@ -1935,6 +1937,11 @@ export interface UiSettingsParams {
readonly?: boolean;
requiresPageReload?: boolean;
type?: UiSettingsType;
+ // Warning: (ae-forgotten-export) The symbol "ImageValidation" needs to be exported by the entry point index.d.ts
+ // Warning: (ae-forgotten-export) The symbol "StringValidation" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
+ validation?: ImageValidation | StringValidation;
value?: SavedObjectAttribute;
}
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index 611842e8a7de0..7c3f9f249db13 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -17,7 +17,6 @@
* under the License.
*/
-import { take } from 'rxjs/operators';
import { Type } from '@kbn/config-schema';
import {
@@ -216,9 +215,6 @@ export class Server {
coreId,
'core',
async (context, req, res): Promise => {
- // it consumes elasticsearch observables to provide the same client throughout the context lifetime.
- const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise();
- const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise();
const savedObjectsClient = coreSetup.savedObjects.getScopedClient(req);
const uiSettingsClient = coreSetup.uiSettings.asScopedToClient(savedObjectsClient);
@@ -230,8 +226,8 @@ export class Server {
client: savedObjectsClient,
},
elasticsearch: {
- adminClient: adminClient.asScoped(req),
- dataClient: dataClient.asScoped(req),
+ adminClient: coreSetup.elasticsearch.adminClient.asScoped(req),
+ dataClient: coreSetup.elasticsearch.dataClient.asScoped(req),
},
uiSettings: {
client: uiSettingsClient,
diff --git a/src/core/server/types.ts b/src/core/server/types.ts
index 9919c7f0386b5..2433aad1a2be5 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 5e3f0a4fbb6bd..14eb71a22cefc 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,25 @@ 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
+ */
+ validation?: ImageValidation | StringValidation;
+}
+
+export interface StringValidation {
+ regexString: string;
+ message: string;
+}
+
+export interface ImageValidation {
+ maxSize: {
+ length: number;
+ description: string;
+ };
}
/** @internal */
diff --git a/src/core/utils/merge.test.ts b/src/core/utils/merge.test.ts
index aa98f51067411..c857e980dec21 100644
--- a/src/core/utils/merge.test.ts
+++ b/src/core/utils/merge.test.ts
@@ -61,4 +61,15 @@ describe('merge', () => {
expect(merge({ a: 0 }, {}, {})).toEqual({ a: 0 });
expect(merge({ a: 0 }, { a: 1 }, {})).toEqual({ a: 1 });
});
+
+ test(`doesn't pollute prototypes`, () => {
+ merge({}, JSON.parse('{ "__proto__": { "foo": "bar" } }'));
+ merge({}, JSON.parse('{ "constructor": { "prototype": { "foo": "bar" } } }'));
+ merge(
+ {},
+ JSON.parse('{ "__proto__": { "foo": "bar" } }'),
+ JSON.parse('{ "constructor": { "prototype": { "foo": "bar" } } }')
+ );
+ expect(({} as any).foo).toBe(undefined);
+ });
});
diff --git a/src/dev/build/README.md b/src/dev/build/README.md
index af08414f0bf4b..3b579033fabe1 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 19d74bcf89e30..52928d6e47fc4 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 ea8cc1e286407..72910226bb04a 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 1fdd7d8d4f5ff..ce305169a777b 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 a4aa3474c0762..bd084767a723f 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 14f03a7a116a6..1ff7c700d0386 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/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 40b9cc4640eef..761a252b56a87 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
@@ -164,7 +164,7 @@ function EditorUI() {
mappings.retrieveAutoCompleteInfo();
- const unsubscribeResizer = subscribeResizeChecker(editorRef.current!, editor.getCoreEditor());
+ const unsubscribeResizer = subscribeResizeChecker(editorRef.current!, editor);
setupAutosave();
return () => {
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/subscribe_console_resize_checker.ts b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/subscribe_console_resize_checker.ts
index 4ecd5d415833c..1adc56d47927b 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/subscribe_console_resize_checker.ts
+++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/subscribe_console_resize_checker.ts
@@ -22,8 +22,15 @@ export function subscribeResizeChecker(el: HTMLElement, ...editors: any[]) {
const checker = new ResizeChecker(el);
checker.on('resize', () =>
editors.forEach(e => {
- e.resize();
- if (e.updateActionsBar) e.updateActionsBar();
+ if (e.getCoreEditor) {
+ e.getCoreEditor().resize();
+ } else {
+ e.resize();
+ }
+
+ if (e.updateActionsBar) {
+ e.updateActionsBar();
+ }
})
);
return () => checker.destroy();
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 608c73335b3e5..8301daa675b5c 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
@@ -297,30 +297,32 @@ export class LegacyCoreEditor implements CoreEditor {
// pageY is relative to page, so subtract the offset
// from pageY to get the new top value
const offsetFromPage = $(this.editor.container).offset()!.top;
- const startRow = range.start.lineNumber - 1;
+ const startLine = range.start.lineNumber;
const startColumn = range.start.column;
- const firstLine = this.getLineValue(startRow);
+ const firstLine = this.getLineValue(startLine);
const maxLineLength = this.getWrapLimit() - 5;
const isWrapping = firstLine.length > maxLineLength;
- const getScreenCoords = (row: number) =>
- this.editor.renderer.textToScreenCoordinates(row, startColumn).pageY - offsetFromPage;
- const topOfReq = getScreenCoords(startRow);
+ const getScreenCoords = (line: number) =>
+ this.editor.renderer.textToScreenCoordinates(line - 1, startColumn).pageY -
+ offsetFromPage +
+ (window.pageYOffset || 0);
+ const topOfReq = getScreenCoords(startLine);
if (topOfReq >= 0) {
let offset = 0;
if (isWrapping) {
// Try get the line height of the text area in pixels.
const textArea = $(this.editor.container.querySelector('textArea')!);
- const hasRoomOnNextLine = this.getLineValue(startRow + 1).length < maxLineLength;
+ const hasRoomOnNextLine = this.getLineValue(startLine).length < maxLineLength;
if (textArea && hasRoomOnNextLine) {
// Line height + the number of wraps we have on a line.
- offset += this.getLineValue(startRow).length * textArea.height()!;
+ offset += this.getLineValue(startLine).length * textArea.height()!;
} else {
- if (startRow > 0) {
- this.setActionsBar(getScreenCoords(startRow - 1));
+ if (startLine > 1) {
+ this.setActionsBar(getScreenCoords(startLine - 1));
return;
}
- this.setActionsBar(getScreenCoords(startRow + 1));
+ this.setActionsBar(getScreenCoords(startLine + 1));
return;
}
}
diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/smart_resize.ts b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/smart_resize.ts
index b88e0e44591d8..7c4d871c4d73e 100644
--- a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/smart_resize.ts
+++ b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/smart_resize.ts
@@ -24,7 +24,7 @@ export default function(editor: any) {
const resize = editor.resize;
const throttledResize = throttle(() => {
- resize.call(editor);
+ resize.call(editor, false);
// Keep current top line in view when resizing to avoid losing user context
const userRow = get(throttledResize, 'topRow', 0);
diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/utils/__tests__/utils_string_expanding.txt b/src/legacy/core_plugins/console/public/np_ready/lib/utils/__tests__/utils_string_expanding.txt
index 88467ab3672cd..7de874c244e74 100644
--- a/src/legacy/core_plugins/console/public/np_ready/lib/utils/__tests__/utils_string_expanding.txt
+++ b/src/legacy/core_plugins/console/public/np_ready/lib/utils/__tests__/utils_string_expanding.txt
@@ -52,3 +52,33 @@ Correctly handle new lines in triple quotes
SELECT * FROM "TABLE"
"""
}
+==========
+Single quotes escaped special case, start and end
+-------------------------------------
+{
+ "query": "\"test\""
+}
+-------------------------------------
+{
+ "query": "\"test\""
+}
+==========
+Single quotes escaped special case, start
+-------------------------------------
+{
+ "query": "\"test"
+}
+-------------------------------------
+{
+ "query": "\"test"
+}
+==========
+Single quotes escaped special case, end
+-------------------------------------
+{
+ "query": "test\""
+}
+-------------------------------------
+{
+ "query": "test\""
+}
diff --git a/src/legacy/core_plugins/console/public/np_ready/lib/utils/utils.ts b/src/legacy/core_plugins/console/public/np_ready/lib/utils/utils.ts
index a7f59acf1d77b..0b10938abe704 100644
--- a/src/legacy/core_plugins/console/public/np_ready/lib/utils/utils.ts
+++ b/src/legacy/core_plugins/console/public/np_ready/lib/utils/utils.ts
@@ -84,6 +84,20 @@ export function expandLiteralStrings(data: string) {
// Expand to triple quotes if there are _any_ slashes
if (string.match(/\\./)) {
const firstDoubleQuoteIdx = string.indexOf('"');
+ const lastDoubleQuoteIdx = string.lastIndexOf('"');
+
+ // Handle a special case where we may have a value like "\"test\"". We don't
+ // want to expand this to """"test"""" - so we terminate before processing the string
+ // further if we detect this either at the start or end of the double quote section.
+
+ if (string[firstDoubleQuoteIdx + 1] === '\\' && string[firstDoubleQuoteIdx + 2] === '"') {
+ return string;
+ }
+
+ if (string[lastDoubleQuoteIdx - 1] === '"' && string[lastDoubleQuoteIdx - 2] === '\\') {
+ return string;
+ }
+
const colonAndAnySpacing = string.slice(0, firstDoubleQuoteIdx);
const rawStringifiedValue = string.slice(firstDoubleQuoteIdx, string.length);
// Remove one level of JSON stringification
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 45da7f054bfb4..e6ca1fb575396 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 55d9673054276..7e6e6692f931b 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 6c0ee8a2425ee..8bbee143c299f 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 aba84d0a10fc2..0852d8d184831 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 3867efd814238..2d1636d5f2c02 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 e96273ffbc083..9ffc4b55f3037 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 0000000000000..10ea433ca68c5
--- /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 31acc86a2fa56..6fbdea0f1244b 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 1bfbaa078b796..68f2e338cd201 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 ceffec26beecc..33720576ef8a3 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 0b0ca087b1819..c2f741066bbdb 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 b8aa5dd4ca711..b3cbbe80e0d00 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 620f1c629d959..c2bed081124a8 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 3e959b9630e98..4ce82a2c25e0e 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 582ecab1dd614..cf5a5c5f32db3 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 4e103b0af2195..43945dfada35c 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 739ea16888146..393197949e86c 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 0000000000000..e9d4a6cee54ce
--- /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 0bbf456245c84..0000000000000
--- 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 55e2c63f608d4..257513f20fa94 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 632fe63e9e148..278811ca85df9 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 31c221b36e2b2..99482a4be2d7b 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 049544b504918..fd13067c84cc0 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 276e765ae7fe6..947c7ae7e6e36 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 fdc18f5bfa0e6..bc017b5a1a871 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 15bbf9c01cd77..ee3570314618a 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 a3fb874b5aa1b..d6d075f452fed 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
new file mode 100644
index 0000000000000..df2dbfd54c130
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/url_helper.test.ts
@@ -0,0 +1,118 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+jest.mock('../', () => ({
+ DashboardConstants: {
+ ADD_EMBEDDABLE_ID: 'addEmbeddableId',
+ ADD_EMBEDDABLE_TYPE: 'addEmbeddableType',
+ },
+}));
+
+jest.mock('../legacy_imports', () => {
+ return {
+ absoluteToParsedUrl: jest.fn(() => {
+ return {
+ basePath: '/pep',
+ appId: 'kibana',
+ appPath: '/dashboard?addEmbeddableType=lens&addEmbeddableId=123eb456cd&x=1&y=2&z=3',
+ hostname: 'localhost',
+ port: 5601,
+ protocol: 'http:',
+ addQueryParameter: () => {},
+ getAbsoluteUrl: () => {
+ return 'http://localhost:5601/pep/app/kibana#/dashboard?addEmbeddableType=lens&addEmbeddableId=123eb456cd&x=1&y=2&z=3';
+ },
+ };
+ }),
+ };
+});
+
+import {
+ addEmbeddableToDashboardUrl,
+ getLensUrlFromDashboardAbsoluteUrl,
+ getUrlVars,
+} from '../np_ready/url_helper';
+
+describe('Dashboard URL Helper', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ });
+
+ it('addEmbeddableToDashboardUrl', () => {
+ const id = '123eb456cd';
+ const type = 'lens';
+ const urlVars = {
+ x: '1',
+ y: '2',
+ z: '3',
+ };
+ const basePath = '/pep';
+ const url =
+ "http://localhost:5601/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()";
+ expect(addEmbeddableToDashboardUrl(url, basePath, id, urlVars, type)).toEqual(
+ `http://localhost:5601/pep/app/kibana#/dashboard?addEmbeddableType=${type}&addEmbeddableId=${id}&x=1&y=2&z=3`
+ );
+ });
+
+ it('getUrlVars', () => {
+ let url =
+ "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()";
+ expect(getUrlVars(url)).toEqual({
+ _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))',
+ _a: "(description:'',filters:!()",
+ });
+ url = 'http://mybusiness.mydomain.com/app/kibana#/dashboard?x=y&y=z';
+ expect(getUrlVars(url)).toEqual({
+ x: 'y',
+ y: 'z',
+ });
+ 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', () => {
+ const id = '1244';
+ const basePath = '/wev';
+ let url =
+ "http://localhost:5601/wev/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()";
+ expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual(
+ 'http://localhost:5601/wev/app/kibana#/lens/edit/1244'
+ );
+
+ url =
+ "http://localhost:5601/wev/app/kibana#/dashboard/625357282?_a=(description:'',filters:!()&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))";
+ expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual(
+ 'http://localhost:5601/wev/app/kibana#/lens/edit/1244'
+ );
+
+ url = 'http://myserver.mydomain.com:5601/wev/app/kibana#/dashboard/777182';
+ expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual(
+ 'http://myserver.mydomain.com:5601/wev/app/kibana#/lens/edit/1244'
+ );
+
+ url =
+ "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()";
+ expect(getLensUrlFromDashboardAbsoluteUrl(url, '', id)).toEqual(
+ 'http://localhost:5601/app/kibana#/lens/edit/1244'
+ );
+ });
+});
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts
index ec0913e5fb3e7..ba01919431080 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts
+++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts
@@ -67,3 +67,4 @@ export { IInjector } from 'ui/chrome';
export { SavedObjectLoader } from 'ui/saved_objects';
export { VISUALIZE_EMBEDDABLE_TYPE } from '../visualize_embeddable';
export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router';
+export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url';
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 2a5dedab98151..7f7bf7cf47bda 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/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx
index 2523d1e60a741..2706b588a2ec4 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx
+++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx
@@ -37,7 +37,6 @@ import {
KbnUrl,
SavedObjectSaveOpts,
unhashUrl,
- VISUALIZE_EMBEDDABLE_TYPE,
} from '../legacy_imports';
import { FilterStateManager } from '../../../../data/public';
import {
@@ -334,13 +333,12 @@ export class DashboardAppController {
// This code needs to be replaced with a better mechanism for adding new embeddables of
// any type from the add panel. Likely this will happen via creating a visualization "inline",
// without navigating away from the UX.
- if ($routeParams[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]) {
- container.addSavedObjectEmbeddable(
- VISUALIZE_EMBEDDABLE_TYPE,
- $routeParams[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]
- );
- kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
- kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM);
+ if ($routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]) {
+ const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE];
+ const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID];
+ container.addSavedObjectEmbeddable(type, id);
+ kbnUrl.removeParam(DashboardConstants.ADD_EMBEDDABLE_TYPE);
+ kbnUrl.removeParam(DashboardConstants.ADD_EMBEDDABLE_ID);
}
}
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts
index b76b3f309874a..fe42e07912799 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts
+++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_constants.ts
@@ -19,9 +19,10 @@
export const DashboardConstants = {
ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM: 'addToDashboard',
- NEW_VISUALIZATION_ID_PARAM: 'addVisualization',
LANDING_PAGE_PATH: '/dashboards',
CREATE_NEW_DASHBOARD_URL: '/dashboard',
+ ADD_EMBEDDABLE_ID: 'addEmbeddableId',
+ ADD_EMBEDDABLE_TYPE: 'addEmbeddableType',
};
export function createDashboardEditUrl(id: string) {
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/listing/__snapshots__/dashboard_listing.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/listing/__snapshots__/dashboard_listing.test.js.snap
index b2f004568841a..2a9a793ba43c4 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/listing/__snapshots__/dashboard_listing.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/listing/__snapshots__/dashboard_listing.test.js.snap
@@ -9,6 +9,7 @@ exports[`after fetch hideWriteControls 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
+ headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1}
noItemsFragment={
@@ -16,13 +17,15 @@ exports[`after fetch hideWriteControls 1`] = `
+
-
+
}
/>
@@ -63,6 +66,7 @@ exports[`after fetch initialFilter 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
+ headingId="dashboardListingHeading"
initialFilter="my dashboard"
listingLimit={1000}
noItemsFragment={
@@ -114,13 +118,15 @@ exports[`after fetch initialFilter 1`] = `
}
iconType="dashboardApp"
title={
-
+
-
+
}
/>
@@ -161,6 +167,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
+ headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1}
noItemsFragment={
@@ -212,13 +219,15 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
}
iconType="dashboardApp"
title={
-
+
-
+
}
/>
@@ -259,6 +268,7 @@ exports[`after fetch renders table rows 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
+ headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1000}
noItemsFragment={
@@ -310,13 +320,15 @@ exports[`after fetch renders table rows 1`] = `
}
iconType="dashboardApp"
title={
-
+
-
+
}
/>
@@ -357,6 +369,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
+ headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1}
noItemsFragment={
@@ -408,13 +421,15 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
}
iconType="dashboardApp"
title={
-
+
-
+
}
/>
@@ -455,6 +470,7 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `
entityName="dashboard"
entityNamePlural="dashboards"
findItems={[Function]}
+ headingId="dashboardListingHeading"
initialFilter=""
listingLimit={1000}
noItemsFragment={
@@ -506,13 +522,15 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `
}
iconType="dashboardApp"
title={
-
+
-
+
}
/>
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/listing/dashboard_listing.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/listing/dashboard_listing.js
index 827fe6eabe784..30bf940069fb7 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/listing/dashboard_listing.js
+++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/listing/dashboard_listing.js
@@ -42,6 +42,7 @@ export class DashboardListing extends React.Component {
return (
+
-
+
}
/>
@@ -90,12 +91,12 @@ export class DashboardListing extends React.Component {
+
-
+
}
body={
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 6def1b1a198b8..e76f65c45e428 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
new file mode 100644
index 0000000000000..2e360567c4653
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts
@@ -0,0 +1,102 @@
+/*
+ * 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 { parse } from 'url';
+import { absoluteToParsedUrl } from '../legacy_imports';
+import { DashboardConstants } from './dashboard_constants';
+/**
+ * Return query params from URL
+ * @param url given url
+ */
+export function getUrlVars(url: string): Record {
+ const vars: Record = {};
+ // @ts-ignore
+ url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(_, key, value) {
+ // @ts-ignore
+ vars[key] = decodeURIComponent(value);
+ });
+ return vars;
+}
+
+/** *
+ * Returns dashboard URL with added embeddableType and embeddableId query params
+ * eg.
+ * input: url: http://localhost:5601/lib/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345, embeddableType: 'lens'
+ * output: http://localhost:5601/lib/app/kibana#dashboard?addEmbeddableType=lens&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))
+ * @param url dasbhoard absolute url
+ * @param embeddableId id of the saved visualization
+ * @param basePath current base path
+ * @param urlVars url query params (optional)
+ * @param embeddableType 'lens' or 'visualization' (optional, default is 'lens')
+ */
+export function addEmbeddableToDashboardUrl(
+ url: string | undefined,
+ basePath: string,
+ embeddableId: string,
+ urlVars?: Record,
+ embeddableType?: string
+): string | null {
+ if (!url) {
+ return null;
+ }
+ const dashboardUrl = getUrlWithoutQueryParams(url);
+ const dashboardParsedUrl = absoluteToParsedUrl(dashboardUrl, basePath);
+ if (urlVars) {
+ const keys = Object.keys(urlVars).sort();
+ keys.forEach(key => {
+ dashboardParsedUrl.addQueryParameter(key, urlVars[key]);
+ });
+ }
+ dashboardParsedUrl.addQueryParameter(
+ DashboardConstants.ADD_EMBEDDABLE_TYPE,
+ embeddableType || 'lens'
+ );
+ dashboardParsedUrl.addQueryParameter(DashboardConstants.ADD_EMBEDDABLE_ID, embeddableId);
+ return dashboardParsedUrl.getAbsoluteUrl();
+}
+
+/**
+ * Return Lens URL from dashboard absolute URL
+ * @param dashboardAbsoluteUrl
+ * @param basePath current base path
+ * @param id Lens id
+ */
+export function getLensUrlFromDashboardAbsoluteUrl(
+ dashboardAbsoluteUrl: string | undefined | null,
+ basePath: string | null | undefined,
+ id: string
+): string | null {
+ if (!dashboardAbsoluteUrl || basePath === null || basePath === undefined) {
+ return null;
+ }
+ const { host, protocol } = parse(dashboardAbsoluteUrl);
+ return `${protocol}//${host}${basePath}/app/kibana#/lens/edit/${id}`;
+}
+
+/**
+ * Returns the portion of the URL without query params
+ * eg.
+ * input: http://localhost:5601/lib/app/kibana#/dashboard?param1=x¶m2=y¶m3=z
+ * output:http://localhost:5601/lib/app/kibana#/dashboard
+ * input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x¶m2=y¶m3=z
+ * output: http://localhost:5601/lib/app/kibana#/dashboard/39292992
+ * @param url url to parse
+ */
+function getUrlWithoutQueryParams(url: string): string {
+ return url.split('?')[0];
+}
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 a6c6d91084625..6054b9f8d03c5 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 3f9bf255aefa9..d4ee9e0e0f287 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/np_ready/components/tutorial/tutorial.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/tutorial.js
index 314ddf2196f06..c7aa5b0f5b2f9 100644
--- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/tutorial.js
+++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/tutorial.js
@@ -363,6 +363,11 @@ class TutorialUi extends React.Component {
);
}
+ let icon = this.state.tutorial.euiIconType;
+ if (icon && icon.includes('/')) {
+ icon = this.props.addBasePath(icon);
+ }
+
const instructions = this.getInstructions();
content = (
@@ -371,7 +376,7 @@ class TutorialUi extends React.Component {
description={this.props.replaceTemplateStrings(this.state.tutorial.longDescription)}
previewUrl={previewUrl}
exportedFieldsUrl={exportedFieldsUrl}
- iconType={this.state.tutorial.euiIconType}
+ iconType={icon}
isBeta={this.state.tutorial.isBeta}
/>
diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial_directory.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial_directory.js
index 06da6f35ee42e..697c1b0468cd1 100644
--- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial_directory.js
+++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial_directory.js
@@ -129,7 +129,7 @@ class TutorialDirectoryUi extends React.Component {
let tutorialCards = tutorialConfigs.map(tutorialConfig => {
// add base path to SVG based icons
let icon = tutorialConfig.euiIconType;
- if (icon != null && icon.includes('/')) {
+ if (icon && icon.includes('/')) {
icon = this.props.addBasePath(icon);
}
diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/ibmmq_logs/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/ibmmq_logs/screenshot.png
new file mode 100644
index 0000000000000..100a8b6ae367c
Binary files /dev/null and b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/ibmmq_logs/screenshot.png differ
diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/ibmmq.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/ibmmq.svg
new file mode 100644
index 0000000000000..ad0cb64b161dd
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/ibmmq.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss
index 3b49af9a4a6a6..dfe4aa1fd3b9f 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 0000000000000..12cc1444101e7
--- /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 0000000000000..33a6100c43975
--- /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 c09995caab669..d52bec8304ff9 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 11c41425a0bb5..f2fb17cdb0d60 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 4716fb8f77633..2da4d84463b29 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 432c57d4f473d..879ea555d3300 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 2aaa291f6122b..4ba0fe480ac42 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 ace06e0420a7c..34ce8394232ed 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 941a0ffded820..c1241d5d7c1e5 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 daac04d07da28..805131042f385 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 10d165d0d69c4..eef8f3fc93d90 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 ae168e76d359b..f4d20b4565880 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
>
maxSize.length);
@@ -565,6 +568,36 @@ export class Field extends PureComponent {
renderDescription(setting) {
let description;
+ let deprecation;
+
+ if (setting.deprecation) {
+ const { links } = npStart.core.docLinks;
+
+ deprecation = (
+ <>
+
+ {
+ 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/components/field/field.test.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js
index 74bb0e25ff52e..07ce6f84d2bb6 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js
@@ -72,10 +72,9 @@ const settings = {
defVal: null,
isCustom: false,
isOverridden: false,
- options: {
+ validation: {
maxSize: {
length: 1000,
- displayName: '1 kB',
description: 'Description for 1 kB',
},
},
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 791f9e400b407..6efb89cfba2b2 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,12 +43,14 @@ export function toEditableConfig({ def, name, value, isCustom, isOverridden }) {
defVal: def.value,
type: getValType(def, value),
description: def.description,
- validation: def.validation
- ? {
- regex: new RegExp(def.validation.regexString),
- message: def.validation.message,
- }
- : undefined,
+ deprecation: def.deprecation,
+ validation:
+ def.validation && def.validation.regexString
+ ? {
+ regex: new RegExp(def.validation.regexString),
+ message: def.validation.message,
+ }
+ : def.validation,
options: def.options,
optionLabels: def.optionLabels,
requiresPageReload: !!def.requiresPageReload,
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 dcd68a26743ab..222b035708976 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 ed9bec9db4112..64653730473cd 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
@@ -35,8 +35,8 @@ import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public';
import { initVisEditorDirective } from './visualization_editor';
import { initVisualizationDirective } from './visualization';
-
import {
+ VISUALIZE_EMBEDDABLE_TYPE,
subscribeWithScope,
absoluteToParsedUrl,
KibanaParsedUrl,
@@ -588,7 +588,11 @@ function VisualizeAppController(
getBasePath()
);
dashboardParsedUrl.addQueryParameter(
- DashboardConstants.NEW_VISUALIZATION_ID_PARAM,
+ DashboardConstants.ADD_EMBEDDABLE_TYPE,
+ VISUALIZE_EMBEDDABLE_TYPE
+ );
+ dashboardParsedUrl.addQueryParameter(
+ DashboardConstants.ADD_EMBEDDABLE_ID,
savedVis.id
);
kbnUrl.change(dashboardParsedUrl.appPath);
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js
index 840e647edcc86..b770625cd3d70 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js
@@ -36,6 +36,7 @@ class VisualizeListingTable extends Component {
const { visualizeCapabilities, uiSettings, toastNotifications } = getServices();
return (
+
-
+
}
/>
@@ -130,12 +131,12 @@ class VisualizeListingTable extends Component {
+
-
+
}
body={
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 0b44c7dc4e860..c75fd2096feab 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/new_vis_modal.test.tsx b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx
index 2005133e6d03e..0ef1b711eafc8 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx
@@ -144,7 +144,7 @@ describe('NewVisModal', () => {
expect(window.location.assign).toBeCalledWith('#/visualize/create?type=vis&foo=true&bar=42');
});
- it('closes if visualization with aliasUrl and addToDashboard in editorParams', () => {
+ it('closes and redirects properly if visualization with aliasUrl and addToDashboard in editorParams', () => {
const onClose = jest.fn();
window.location.assign = jest.fn();
const wrapper = mountWithIntl(
@@ -160,7 +160,7 @@ describe('NewVisModal', () => {
);
const visButton = wrapper.find('button[data-test-subj="visType-visWithAliasUrl"]');
visButton.simulate('click');
- expect(window.location.assign).toBeCalledWith('testbasepath/aliasUrl');
+ expect(window.location.assign).toBeCalledWith('testbasepath/aliasUrl?addToDashboard');
expect(onClose).toHaveBeenCalled();
});
});
diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx
index 9e8f46407f591..082fc3bc36b6b 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx
+++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx
@@ -143,15 +143,18 @@ class NewVisModal extends React.Component {
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 107cbc0e754b5..2f7effb7a33c8 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 28cafde45a714..44da7cc8f2c45 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 fc91742c53cca..b7a3a0f000d72 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/telemetry/common/constants.ts b/src/legacy/core_plugins/telemetry/common/constants.ts
index 7e366676a8565..cb4ff79969a32 100644
--- a/src/legacy/core_plugins/telemetry/common/constants.ts
+++ b/src/legacy/core_plugins/telemetry/common/constants.ts
@@ -75,3 +75,9 @@ export const UI_METRIC_USAGE_TYPE = 'ui_metric';
* Link to Advanced Settings.
*/
export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings';
+
+/**
+ * The type name used within the Monitoring index to publish management stats.
+ * @type {string}
+ */
+export const KIBANA_MANAGEMENT_STATS_TYPE = 'management';
diff --git a/src/legacy/core_plugins/telemetry/server/collectors/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/index.ts
index 2f2a53278117b..04ee4773cd60d 100644
--- a/src/legacy/core_plugins/telemetry/server/collectors/index.ts
+++ b/src/legacy/core_plugins/telemetry/server/collectors/index.ts
@@ -22,3 +22,4 @@ export { registerTelemetryUsageCollector } from './usage';
export { registerUiMetricUsageCollector } from './ui_metric';
export { registerLocalizationUsageCollector } from './localization';
export { registerTelemetryPluginUsageCollector } from './telemetry_plugin';
+export { registerManagementUsageCollector } from './management';
diff --git a/src/legacy/core_plugins/telemetry/server/collectors/management/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/management/index.ts
new file mode 100644
index 0000000000000..979bbed3765e2
--- /dev/null
+++ b/src/legacy/core_plugins/telemetry/server/collectors/management/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 { registerManagementUsageCollector } from './telemetry_management_collector';
diff --git a/src/legacy/core_plugins/telemetry/server/collectors/management/telemetry_management_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/management/telemetry_management_collector.ts
new file mode 100644
index 0000000000000..f45cf7fc6bb33
--- /dev/null
+++ b/src/legacy/core_plugins/telemetry/server/collectors/management/telemetry_management_collector.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 { Server } from 'hapi';
+import { size } from 'lodash';
+import { KIBANA_MANAGEMENT_STATS_TYPE } from '../../../common/constants';
+import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
+import { SavedObjectsClient } from '../../../../../../core/server';
+
+export type UsageStats = Record;
+
+export async function getTranslationCount(loader: any, locale: string): Promise {
+ const translations = await loader.getTranslationsByLocale(locale);
+ return size(translations.messages);
+}
+
+export function createCollectorFetch(server: Server) {
+ return async function fetchUsageStats(): Promise {
+ const internalRepo = server.newPlatform.setup.core.savedObjects.createInternalRepository();
+ const uiSettingsClient = server.newPlatform.start.core.uiSettings.asScopedToClient(
+ new SavedObjectsClient(internalRepo)
+ );
+
+ const user = await uiSettingsClient.getUserProvided();
+ const modifiedEntries = Object.keys(user)
+ .filter((key: string) => key !== 'buildNum')
+ .reduce((obj: any, key: string) => {
+ obj[key] = user[key].userValue;
+ return obj;
+ }, {});
+
+ return modifiedEntries;
+ };
+}
+
+export function registerManagementUsageCollector(
+ usageCollection: UsageCollectionSetup,
+ server: any
+) {
+ const collector = usageCollection.makeUsageCollector({
+ type: KIBANA_MANAGEMENT_STATS_TYPE,
+ isReady: () => true,
+ fetch: createCollectorFetch(server),
+ });
+
+ usageCollection.registerCollector(collector);
+}
diff --git a/src/legacy/core_plugins/telemetry/server/plugin.ts b/src/legacy/core_plugins/telemetry/server/plugin.ts
index 06a974f473498..b5b53b1daba55 100644
--- a/src/legacy/core_plugins/telemetry/server/plugin.ts
+++ b/src/legacy/core_plugins/telemetry/server/plugin.ts
@@ -27,6 +27,7 @@ import {
registerTelemetryUsageCollector,
registerLocalizationUsageCollector,
registerTelemetryPluginUsageCollector,
+ registerManagementUsageCollector,
} from './collectors';
export interface PluginsSetup {
@@ -50,5 +51,6 @@ export class TelemetryPlugin {
registerLocalizationUsageCollector(usageCollection, server);
registerTelemetryUsageCollector(usageCollection, server);
registerUiMetricUsageCollector(usageCollection, server);
+ registerManagementUsageCollector(usageCollection, server);
}
}
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 02b1f5fec9c19..2f8a2264530d5 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
diff --git a/src/legacy/core_plugins/vis_type_timeseries/index.ts b/src/legacy/core_plugins/vis_type_timeseries/index.ts
index 9ca14b28e19b2..a502bb174bc99 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 ffd4d08204a7e..654e7d9da4dca 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 8740f84dab3b9..225d81b71b8e0 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 7b42ae8098016..ae6eebc00fc1b 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 0000000000000..3aca50b5b4710
--- /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 d2ded81309ffa..32e87f5a3f666 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 44cd0d320931f..cc28886794f03 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/data_model/__tests__/vega_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js
index de2d913221451..c442f8f17884a 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: '#5BBAA0' },
},
})
);
@@ -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: '#5BBAA0' },
+ area: { fill: '#5BBAA0' },
+ line: { stroke: '#5BBAA0' },
+ path: { stroke: '#5BBAA0' },
+ rect: { fill: '#5BBAA0' },
+ rule: { stroke: '#5BBAA0' },
+ shape: { stroke: '#5BBAA0' },
+ symbol: { fill: '#5BBAA0' },
+ trail: { fill: '#5BBAA0' },
},
})
);
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 cc2ab133941db..ab1664d612b35 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/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 463c1bfb975f5..1f77660c9784c 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 d7026df761d94..7e49e93e4cc4f 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 04d59640554fd..cb570144fcee3 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 9722a01986434..ef11d70926ad7 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 fea665a918f06..30d1de270522e 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 2891b99bba30c..2bfb0bbd15013 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 4b246fecb8146..c727f54874db4 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 39189caeedb32..849e307f7b527 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 25cbbb7c8684b..73a7c1141c601 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 f811ad9162728..cf61b6140f42c 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/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 a176295260c44..b51c25952580a 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 74c952dbf059f..b3a2c058de976 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/vislib/visualizations/point_series/_point_series.js b/src/legacy/ui/public/vislib/visualizations/point_series/_point_series.js
index b9f50b6d941e6..b882048a6b269 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 f0ca97f473324..a1c3a153a196c 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 85b6de26b9516..72dd97ff58642 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 a935270d23fce..4158af19bd858 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 7d558367c032d..9889c1f71c3bf 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 ecf5def5aa6ca..c7ab2fe30dd14 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 584bf0c9e3d35..0c286896d0b71 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 cb941d2ba5683..484c7dfbfd595 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 15957210b3d43..b6784c1734a17 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 7ab7d7653eb5e..4ec29ca409b80 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",
},
@@ -460,6 +463,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",
},
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 6f5f9b3956187..4c8edd85eb559 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",
},
@@ -896,6 +899,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",
},
@@ -1070,11 +1076,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 +1096,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 16b22a164f2f0..960a843f98ab9 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/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 5abce10c5be61..fb56bf0e4255e 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 f9bdf3a25adaa..0c5d2b0c24831 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 {
@@ -43,7 +43,7 @@ interface FieldIconProps {
className?: string;
}
-const { colors } = palettes.euiPaletteColorBlind;
+const colors = euiPaletteColorBlind();
// defaultIcon => a unknown datatype
const defaultIcon = { icon: 'questionInCircle', color: colors[0] };
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 978705a3ad096..18f84f41d5d99 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/kibana_react/public/saved_objects/saved_object_finder.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx
index bd2beaf77a305..1522c6b42824c 100644
--- a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx
+++ b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx
@@ -346,6 +346,9 @@ class SavedObjectFinderUi extends React.Component<
placeholder={i18n.translate('kibana-react.savedObjects.finder.searchPlaceholder', {
defaultMessage: 'Search…',
})}
+ aria-label={i18n.translate('kibana-react.savedObjects.finder.searchPlaceholder', {
+ defaultMessage: 'Search…',
+ })}
fullWidth
value={this.state.query}
onChange={e => {
diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx
index 2e7b22a14fb0e..4c2dac4f39134 100644
--- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx
+++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx
@@ -67,6 +67,11 @@ export interface TableListViewProps {
tableListTitle: string;
toastNotifications: ToastsStart;
uiSettings: IUiSettingsClient;
+ /**
+ * Id of the heading element describing the table. This id will be used as `aria-labelledby` of the wrapper element.
+ * If the table is not empty, this component renders its own h1 element using the same id.
+ */
+ headingId?: string;
}
export interface TableListViewState {
@@ -463,7 +468,7 @@ class TableListView extends React.Component
- {this.props.tableListTitle}
+ {this.props.tableListTitle}
@@ -498,7 +503,11 @@ class TableListView extends React.Component
- {this.renderPageContent()}
+
+ {this.renderPageContent()}
+
);
}
diff --git a/src/plugins/kibana_utils/demos/demos.test.ts b/src/plugins/kibana_utils/demos/demos.test.ts
index 5c50e152ad46c..b905aeff41f1f 100644
--- a/src/plugins/kibana_utils/demos/demos.test.ts
+++ b/src/plugins/kibana_utils/demos/demos.test.ts
@@ -38,7 +38,7 @@ describe('demos', () => {
describe('state sync', () => {
test('url sync demo works', async () => {
expect(await urlSyncResult).toMatchInlineSnapshot(
- `"http://localhost/#?_s=!((completed:!f,id:0,text:'Learning%20state%20containers'),(completed:!f,id:2,text:test))"`
+ `"http://localhost/#?_s=(todos:!((completed:!f,id:0,text:'Learning%20state%20containers'),(completed:!f,id:2,text:test)))"`
);
});
});
diff --git a/src/plugins/kibana_utils/demos/state_containers/counter.ts b/src/plugins/kibana_utils/demos/state_containers/counter.ts
index 643763cc4cee9..4ddf532c1506d 100644
--- a/src/plugins/kibana_utils/demos/state_containers/counter.ts
+++ b/src/plugins/kibana_utils/demos/state_containers/counter.ts
@@ -19,14 +19,24 @@
import { createStateContainer } from '../../public/state_containers';
-const container = createStateContainer(0, {
- increment: (cnt: number) => (by: number) => cnt + by,
- double: (cnt: number) => () => cnt * 2,
-});
+interface State {
+ count: number;
+}
+
+const container = createStateContainer(
+ { count: 0 },
+ {
+ increment: (state: State) => (by: number) => ({ count: state.count + by }),
+ double: (state: State) => () => ({ count: state.count * 2 }),
+ },
+ {
+ count: (state: State) => () => state.count,
+ }
+);
container.transitions.increment(5);
container.transitions.double();
-console.log(container.get()); // eslint-disable-line
+console.log(container.selectors.count()); // eslint-disable-line
-export const result = container.get();
+export const result = container.selectors.count();
diff --git a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts
index 6d0c960e2a5b2..e807783a56f31 100644
--- a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts
+++ b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts
@@ -25,15 +25,19 @@ export interface TodoItem {
id: number;
}
-export type TodoState = TodoItem[];
+export interface TodoState {
+ todos: TodoItem[];
+}
-export const defaultState: TodoState = [
- {
- id: 0,
- text: 'Learning state containers',
- completed: false,
- },
-];
+export const defaultState: TodoState = {
+ todos: [
+ {
+ id: 0,
+ text: 'Learning state containers',
+ completed: false,
+ },
+ ],
+};
export interface TodoActions {
add: PureTransition;
@@ -44,17 +48,34 @@ export interface TodoActions {
clearCompleted: PureTransition;
}
+export interface TodosSelectors {
+ todos: (state: TodoState) => () => TodoItem[];
+ todo: (state: TodoState) => (id: number) => TodoItem | null;
+}
+
export const pureTransitions: TodoActions = {
- add: state => todo => [...state, todo],
- edit: state => todo => state.map(item => (item.id === todo.id ? { ...item, ...todo } : item)),
- delete: state => id => state.filter(item => item.id !== id),
- complete: state => id =>
- state.map(item => (item.id === id ? { ...item, completed: true } : item)),
- completeAll: state => () => state.map(item => ({ ...item, completed: true })),
- clearCompleted: state => () => state.filter(({ completed }) => !completed),
+ add: state => todo => ({ todos: [...state.todos, todo] }),
+ edit: state => todo => ({
+ todos: state.todos.map(item => (item.id === todo.id ? { ...item, ...todo } : item)),
+ }),
+ delete: state => id => ({ todos: state.todos.filter(item => item.id !== id) }),
+ complete: state => id => ({
+ todos: state.todos.map(item => (item.id === id ? { ...item, completed: true } : item)),
+ }),
+ completeAll: state => () => ({ todos: state.todos.map(item => ({ ...item, completed: true })) }),
+ clearCompleted: state => () => ({ todos: state.todos.filter(({ completed }) => !completed) }),
+};
+
+export const pureSelectors: TodosSelectors = {
+ todos: state => () => state.todos,
+ todo: state => id => state.todos.find(todo => todo.id === id) ?? null,
};
-const container = createStateContainer(defaultState, pureTransitions);
+const container = createStateContainer(
+ defaultState,
+ pureTransitions,
+ pureSelectors
+);
container.transitions.add({
id: 1,
@@ -64,6 +85,6 @@ container.transitions.add({
container.transitions.complete(0);
container.transitions.complete(1);
-console.log(container.get()); // eslint-disable-line
+console.log(container.selectors.todos()); // eslint-disable-line
-export const result = container.get();
+export const result = container.selectors.todos();
diff --git a/src/plugins/kibana_utils/demos/state_sync/url.ts b/src/plugins/kibana_utils/demos/state_sync/url.ts
index 657b64f55a776..2c426cae6733a 100644
--- a/src/plugins/kibana_utils/demos/state_sync/url.ts
+++ b/src/plugins/kibana_utils/demos/state_sync/url.ts
@@ -18,7 +18,7 @@
*/
import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc';
-import { BaseStateContainer, createStateContainer } from '../../public/state_containers';
+import { BaseState, BaseStateContainer, createStateContainer } from '../../public/state_containers';
import {
createKbnUrlStateStorage,
syncState,
@@ -55,7 +55,7 @@ export const result = Promise.resolve()
return window.location.href;
});
-function withDefaultState(
+function withDefaultState(
// eslint-disable-next-line no-shadow
stateContainer: BaseStateContainer,
// eslint-disable-next-line no-shadow
diff --git a/src/plugins/kibana_utils/docs/state_containers/README.md b/src/plugins/kibana_utils/docs/state_containers/README.md
index 3b7a8b8bd4621..583f8f65ce6b6 100644
--- a/src/plugins/kibana_utils/docs/state_containers/README.md
+++ b/src/plugins/kibana_utils/docs/state_containers/README.md
@@ -18,14 +18,21 @@ your services or apps.
```ts
import { createStateContainer } from 'src/plugins/kibana_utils';
-const container = createStateContainer(0, {
- increment: (cnt: number) => (by: number) => cnt + by,
- double: (cnt: number) => () => cnt * 2,
-});
+const container = createStateContainer(
+ { count: 0 },
+ {
+ increment: (state: {count: number}) => (by: number) => ({ count: state.count + by }),
+ double: (state: {count: number}) => () => ({ count: state.count * 2 }),
+ },
+ {
+ count: (state: {count: number}) => () => state.count,
+ }
+);
container.transitions.increment(5);
container.transitions.double();
-console.log(container.get()); // 10
+
+console.log(container.selectors.count()); // 10
```
diff --git a/src/plugins/kibana_utils/docs/state_containers/creation.md b/src/plugins/kibana_utils/docs/state_containers/creation.md
index 66d28bbd8603f..f8ded75ed3f45 100644
--- a/src/plugins/kibana_utils/docs/state_containers/creation.md
+++ b/src/plugins/kibana_utils/docs/state_containers/creation.md
@@ -32,7 +32,7 @@ Create your a state container.
```ts
import { createStateContainer } from 'src/plugins/kibana_utils';
-const container = createStateContainer(defaultState, {});
+const container = createStateContainer(defaultState);
console.log(container.get());
```
diff --git a/src/plugins/kibana_utils/docs/state_containers/no_react.md b/src/plugins/kibana_utils/docs/state_containers/no_react.md
index 7a15483d83b44..a72995f4f1eae 100644
--- a/src/plugins/kibana_utils/docs/state_containers/no_react.md
+++ b/src/plugins/kibana_utils/docs/state_containers/no_react.md
@@ -1,13 +1,13 @@
# Consuming state in non-React setting
-To read the current `state` of the store use `.get()` method.
+To read the current `state` of the store use `.get()` method or `getState()` alias method.
```ts
-store.get();
+stateContainer.get();
```
To listen for latest state changes use `.state$` observable.
```ts
-store.state$.subscribe(state => { ... });
+stateContainer.state$.subscribe(state => { ... });
```
diff --git a/src/plugins/kibana_utils/docs/state_containers/react.md b/src/plugins/kibana_utils/docs/state_containers/react.md
index 363fd9253d44f..1bab1af1d5f68 100644
--- a/src/plugins/kibana_utils/docs/state_containers/react.md
+++ b/src/plugins/kibana_utils/docs/state_containers/react.md
@@ -9,7 +9,7 @@
```ts
import { createStateContainer, createStateContainerReactHelpers } from 'src/plugins/kibana_utils';
-const container = createStateContainer({}, {});
+const container = createStateContainer({});
export const {
Provider,
Consumer,
diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts
index 95f4c35f2ce01..d4877acaa5ca0 100644
--- a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts
+++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts
@@ -19,18 +19,9 @@
import { createStateContainer } from './create_state_container';
-const create = (state: S, transitions: T = {} as T) => {
- const pureTransitions = {
- set: () => (newState: S) => newState,
- ...transitions,
- };
- const store = createStateContainer(state, pureTransitions);
- return { store, mutators: store.transitions };
-};
-
-test('can create store', () => {
- const { store } = create({});
- expect(store).toMatchObject({
+test('can create state container', () => {
+ const stateContainer = createStateContainer({});
+ expect(stateContainer).toMatchObject({
getState: expect.any(Function),
state$: expect.any(Object),
transitions: expect.any(Object),
@@ -45,9 +36,9 @@ test('can set default state', () => {
const defaultState = {
foo: 'bar',
};
- const { store } = create(defaultState);
- expect(store.get()).toEqual(defaultState);
- expect(store.getState()).toEqual(defaultState);
+ const stateContainer = createStateContainer(defaultState);
+ expect(stateContainer.get()).toEqual(defaultState);
+ expect(stateContainer.getState()).toEqual(defaultState);
});
test('can set state', () => {
@@ -57,12 +48,12 @@ test('can set state', () => {
const newState = {
foo: 'baz',
};
- const { store, mutators } = create(defaultState);
+ const stateContainer = createStateContainer(defaultState);
- mutators.set(newState);
+ stateContainer.set(newState);
- expect(store.get()).toEqual(newState);
- expect(store.getState()).toEqual(newState);
+ expect(stateContainer.get()).toEqual(newState);
+ expect(stateContainer.getState()).toEqual(newState);
});
test('does not shallow merge states', () => {
@@ -72,22 +63,22 @@ test('does not shallow merge states', () => {
const newState = {
foo2: 'baz',
};
- const { store, mutators } = create(defaultState);
+ const stateContainer = createStateContainer(defaultState);
- mutators.set(newState as any);
+ stateContainer.set(newState as any);
- expect(store.get()).toEqual(newState);
- expect(store.getState()).toEqual(newState);
+ expect(stateContainer.get()).toEqual(newState);
+ expect(stateContainer.getState()).toEqual(newState);
});
test('can subscribe and unsubscribe to state changes', () => {
- const { store, mutators } = create({});
+ const stateContainer = createStateContainer({});
const spy = jest.fn();
- const subscription = store.state$.subscribe(spy);
- mutators.set({ a: 1 });
- mutators.set({ a: 2 });
+ const subscription = stateContainer.state$.subscribe(spy);
+ stateContainer.set({ a: 1 });
+ stateContainer.set({ a: 2 });
subscription.unsubscribe();
- mutators.set({ a: 3 });
+ stateContainer.set({ a: 3 });
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0]).toEqual({ a: 1 });
@@ -95,16 +86,16 @@ test('can subscribe and unsubscribe to state changes', () => {
});
test('multiple subscribers can subscribe', () => {
- const { store, mutators } = create({});
+ const stateContainer = createStateContainer({});
const spy1 = jest.fn();
const spy2 = jest.fn();
- const subscription1 = store.state$.subscribe(spy1);
- const subscription2 = store.state$.subscribe(spy2);
- mutators.set({ a: 1 });
+ const subscription1 = stateContainer.state$.subscribe(spy1);
+ const subscription2 = stateContainer.state$.subscribe(spy2);
+ stateContainer.set({ a: 1 });
subscription1.unsubscribe();
- mutators.set({ a: 2 });
+ stateContainer.set({ a: 2 });
subscription2.unsubscribe();
- mutators.set({ a: 3 });
+ stateContainer.set({ a: 3 });
expect(spy1).toHaveBeenCalledTimes(1);
expect(spy2).toHaveBeenCalledTimes(2);
@@ -120,19 +111,19 @@ test('can create state container without transitions', () => {
expect(stateContainer.get()).toEqual(state);
});
-test('creates impure mutators from pure mutators', () => {
- const { mutators } = create(
+test('creates transitions', () => {
+ const stateContainer = createStateContainer(
{},
{
setFoo: () => (bar: any) => ({ foo: bar }),
}
);
- expect(typeof mutators.setFoo).toBe('function');
+ expect(typeof stateContainer.transitions.setFoo).toBe('function');
});
-test('mutators can update state', () => {
- const { store, mutators } = create(
+test('transitions can update state', () => {
+ const stateContainer = createStateContainer(
{
value: 0,
foo: 'bar',
@@ -143,30 +134,30 @@ test('mutators can update state', () => {
}
);
- expect(store.get()).toEqual({
+ expect(stateContainer.get()).toEqual({
value: 0,
foo: 'bar',
});
- mutators.add(11);
- mutators.setFoo('baz');
+ stateContainer.transitions.add(11);
+ stateContainer.transitions.setFoo('baz');
- expect(store.get()).toEqual({
+ expect(stateContainer.get()).toEqual({
value: 11,
foo: 'baz',
});
- mutators.add(-20);
- mutators.setFoo('bazooka');
+ stateContainer.transitions.add(-20);
+ stateContainer.transitions.setFoo('bazooka');
- expect(store.get()).toEqual({
+ expect(stateContainer.get()).toEqual({
value: -9,
foo: 'bazooka',
});
});
-test('mutators methods are not bound', () => {
- const { store, mutators } = create(
+test('transitions methods are not bound', () => {
+ const stateContainer = createStateContainer(
{ value: -3 },
{
add: (state: { value: number }) => (increment: number) => ({
@@ -176,13 +167,13 @@ test('mutators methods are not bound', () => {
}
);
- expect(store.get()).toEqual({ value: -3 });
- mutators.add(4);
- expect(store.get()).toEqual({ value: 1 });
+ expect(stateContainer.get()).toEqual({ value: -3 });
+ stateContainer.transitions.add(4);
+ expect(stateContainer.get()).toEqual({ value: 1 });
});
-test('created mutators are saved in store object', () => {
- const { store, mutators } = create(
+test('created transitions are saved in stateContainer object', () => {
+ const stateContainer = createStateContainer(
{ value: -3 },
{
add: (state: { value: number }) => (increment: number) => ({
@@ -192,55 +183,57 @@ test('created mutators are saved in store object', () => {
}
);
- expect(typeof store.transitions.add).toBe('function');
- mutators.add(5);
- expect(store.get()).toEqual({ value: 2 });
+ expect(typeof stateContainer.transitions.add).toBe('function');
+ stateContainer.transitions.add(5);
+ expect(stateContainer.get()).toEqual({ value: 2 });
});
-test('throws when state is modified inline - 1', () => {
- const container = createStateContainer({ a: 'b' }, {});
+test('throws when state is modified inline', () => {
+ const container = createStateContainer({ a: 'b', array: [{ a: 'b' }] });
- let error: TypeError | null = null;
- try {
+ expect(() => {
(container.get().a as any) = 'c';
- } catch (err) {
- error = err;
- }
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"Cannot assign to read only property 'a' of object '#'"`
+ );
- expect(error).toBeInstanceOf(TypeError);
-});
+ expect(() => {
+ (container.getState().a as any) = 'c';
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"Cannot assign to read only property 'a' of object '#'"`
+ );
-test('throws when state is modified inline - 2', () => {
- const container = createStateContainer({ a: 'b' }, {});
+ expect(() => {
+ (container.getState().array as any).push('c');
+ }).toThrowErrorMatchingInlineSnapshot(`"Cannot add property 1, object is not extensible"`);
- let error: TypeError | null = null;
- try {
- (container.getState().a as any) = 'c';
- } catch (err) {
- error = err;
- }
+ expect(() => {
+ (container.getState().array[0] as any).c = 'b';
+ }).toThrowErrorMatchingInlineSnapshot(`"Cannot add property c, object is not extensible"`);
- expect(error).toBeInstanceOf(TypeError);
+ expect(() => {
+ container.set(null as any);
+ expect(container.getState()).toBeNull();
+ }).not.toThrow();
});
-test('throws when state is modified inline in subscription', done => {
+test('throws when state is modified inline in subscription', () => {
const container = createStateContainer({ a: 'b' }, { set: () => (newState: any) => newState });
container.subscribe(value => {
- let error: TypeError | null = null;
- try {
+ expect(() => {
(value.a as any) = 'd';
- } catch (err) {
- error = err;
- }
- expect(error).toBeInstanceOf(TypeError);
- done();
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"Cannot assign to read only property 'a' of object '#'"`
+ );
});
+
container.transitions.set({ a: 'c' });
});
describe('selectors', () => {
test('can specify no selectors, or can skip them', () => {
+ createStateContainer({});
createStateContainer({}, {});
createStateContainer({}, {}, {});
});
diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts
index b949a9daed0ae..d420aec30f068 100644
--- a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts
+++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts
@@ -20,34 +20,52 @@
import { BehaviorSubject } from 'rxjs';
import { skip } from 'rxjs/operators';
import { RecursiveReadonly } from '@kbn/utility-types';
+import deepFreeze from 'deep-freeze-strict';
import {
PureTransitionsToTransitions,
PureTransition,
ReduxLikeStateContainer,
PureSelectorsToSelectors,
+ BaseState,
} from './types';
const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable';
+const $$setActionType = '@@SET';
const freeze: (value: T) => RecursiveReadonly =
process.env.NODE_ENV !== 'production'
? (value: T): RecursiveReadonly => {
- if (!value) return value as RecursiveReadonly;
- if (value instanceof Array) return value as RecursiveReadonly;
- if (typeof value === 'object') return Object.freeze({ ...value }) as RecursiveReadonly;
- else return value as RecursiveReadonly;
+ const isFreezable = value !== null && typeof value === 'object';
+ if (isFreezable) return deepFreeze(value) as RecursiveReadonly;
+ return value as RecursiveReadonly;
}
: (value: T) => value as RecursiveReadonly;
-export const createStateContainer = <
- State,
- PureTransitions extends object = {},
- PureSelectors extends object = {}
+export function createStateContainer(
+ defaultState: State
+): ReduxLikeStateContainer;
+export function createStateContainer(
+ defaultState: State,
+ pureTransitions: PureTransitions
+): ReduxLikeStateContainer;
+export function createStateContainer<
+ State extends BaseState,
+ PureTransitions extends object,
+ PureSelectors extends object
+>(
+ defaultState: State,
+ pureTransitions: PureTransitions,
+ pureSelectors: PureSelectors
+): ReduxLikeStateContainer;
+export function createStateContainer<
+ State extends BaseState,
+ PureTransitions extends object,
+ PureSelectors extends object
>(
defaultState: State,
pureTransitions: PureTransitions = {} as PureTransitions,
pureSelectors: PureSelectors = {} as PureSelectors
-): ReduxLikeStateContainer => {
+): ReduxLikeStateContainer {
const data$ = new BehaviorSubject>(freeze(defaultState));
const state$ = data$.pipe(skip(1));
const get = () => data$.getValue();
@@ -56,9 +74,13 @@ export const createStateContainer = <
state$,
getState: () => data$.getValue(),
set: (state: State) => {
- data$.next(freeze(state));
+ container.dispatch({ type: $$setActionType, args: [state] });
},
reducer: (state, action) => {
+ if (action.type === $$setActionType) {
+ return freeze(action.args[0] as State);
+ }
+
const pureTransition = (pureTransitions as Record>)[
action.type
];
@@ -86,4 +108,4 @@ export const createStateContainer = <
[$$observable]: state$,
};
return container;
-};
+}
diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx
index c1a35441b637b..0f25f65c30ade 100644
--- a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx
+++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx
@@ -23,15 +23,6 @@ import { act, Simulate } from 'react-dom/test-utils';
import { createStateContainer } from './create_state_container';
import { createStateContainerReactHelpers } from './create_state_container_react_helpers';
-const create = (state: S, transitions: T = {} as T) => {
- const pureTransitions = {
- set: () => (newState: S) => newState,
- ...transitions,
- };
- const store = createStateContainer(state, pureTransitions);
- return { store, mutators: store.transitions };
-};
-
let container: HTMLDivElement | null;
beforeEach(() => {
@@ -56,12 +47,12 @@ test('can create React context', () => {
});
test(' passes state to ', () => {
- const { store } = create({ hello: 'world' });
- const { Provider, Consumer } = createStateContainerReactHelpers();
+ const stateContainer = createStateContainer({ hello: 'world' });
+ const { Provider, Consumer } = createStateContainerReactHelpers();
ReactDOM.render(
-
- {(s: typeof store) => s.get().hello}
+
+ {(s: typeof stateContainer) => s.get().hello}
,
container
);
@@ -79,8 +70,8 @@ interface Props1 {
}
test(' passes state to connect()()', () => {
- const { store } = create({ hello: 'Bob' });
- const { Provider, connect } = createStateContainerReactHelpers();
+ const stateContainer = createStateContainer({ hello: 'Bob' });
+ const { Provider, connect } = createStateContainerReactHelpers();
const Demo: React.FC = ({ message, stop }) => (
<>
@@ -92,7 +83,7 @@ test(' passes state to connect()()', () => {
const DemoConnected = connect(mergeProps)(Demo);
ReactDOM.render(
-
+
,
container
@@ -101,14 +92,14 @@ test(' passes state to connect()()', () => {
expect(container!.innerHTML).toBe('Bob?');
});
-test('context receives Redux store', () => {
- const { store } = create({ foo: 'bar' });
- const { Provider, context } = createStateContainerReactHelpers();
+test('context receives stateContainer', () => {
+ const stateContainer = createStateContainer({ foo: 'bar' });
+ const { Provider, context } = createStateContainerReactHelpers();
ReactDOM.render(
/* eslint-disable no-shadow */
-
- {store => store.get().foo}
+
+ {stateContainer => stateContainer.get().foo}
,
/* eslint-enable no-shadow */
container
@@ -117,21 +108,21 @@ test('context receives Redux store', () => {
expect(container!.innerHTML).toBe('bar');
});
-xtest('can use multiple stores in one React app', () => {});
+test.todo('can use multiple stores in one React app');
describe('hooks', () => {
describe('useStore', () => {
- test('can select store using useStore hook', () => {
- const { store } = create({ foo: 'bar' });
- const { Provider, useContainer } = createStateContainerReactHelpers();
+ test('can select store using useContainer hook', () => {
+ const stateContainer = createStateContainer({ foo: 'bar' });
+ const { Provider, useContainer } = createStateContainerReactHelpers();
const Demo: React.FC<{}> = () => {
// eslint-disable-next-line no-shadow
- const store = useContainer();
- return <>{store.get().foo}>;
+ const stateContainer = useContainer();
+ return <>{stateContainer.get().foo}>;
};
ReactDOM.render(
-
+
,
container
@@ -143,15 +134,15 @@ describe('hooks', () => {
describe('useState', () => {
test('can select state using useState hook', () => {
- const { store } = create({ foo: 'qux' });
- const { Provider, useState } = createStateContainerReactHelpers();
+ const stateContainer = createStateContainer({ foo: 'qux' });
+ const { Provider, useState } = createStateContainerReactHelpers();
const Demo: React.FC<{}> = () => {
const { foo } = useState();
return <>{foo}>;
};
ReactDOM.render(
-
+
,
container
@@ -161,23 +152,20 @@ describe('hooks', () => {
});
test('re-renders when state changes', () => {
- const {
- store,
- mutators: { setFoo },
- } = create(
+ const stateContainer = createStateContainer(
{ foo: 'bar' },
{
setFoo: (state: { foo: string }) => (foo: string) => ({ ...state, foo }),
}
);
- const { Provider, useState } = createStateContainerReactHelpers();
+ const { Provider, useState } = createStateContainerReactHelpers();
const Demo: React.FC<{}> = () => {
const { foo } = useState();
return <>{foo}>;
};
ReactDOM.render(
-
+
,
container
@@ -185,7 +173,7 @@ describe('hooks', () => {
expect(container!.innerHTML).toBe('bar');
act(() => {
- setFoo('baz');
+ stateContainer.transitions.setFoo('baz');
});
expect(container!.innerHTML).toBe('baz');
});
@@ -193,7 +181,7 @@ describe('hooks', () => {
describe('useTransitions', () => {
test('useTransitions hook returns mutations that can update state', () => {
- const { store } = create(
+ const stateContainer = createStateContainer(
{
cnt: 0,
},
@@ -206,7 +194,7 @@ describe('hooks', () => {
);
const { Provider, useState, useTransitions } = createStateContainerReactHelpers<
- typeof store
+ typeof stateContainer
>();
const Demo: React.FC<{}> = () => {
const { cnt } = useState();
@@ -220,7 +208,7 @@ describe('hooks', () => {
};
ReactDOM.render(
-
+
,
container
@@ -240,7 +228,7 @@ describe('hooks', () => {
describe('useSelector', () => {
test('can select deeply nested value', () => {
- const { store } = create({
+ const stateContainer = createStateContainer({
foo: {
bar: {
baz: 'qux',
@@ -248,14 +236,14 @@ describe('hooks', () => {
},
});
const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz;
- const { Provider, useSelector } = createStateContainerReactHelpers();
+ const { Provider, useSelector } = createStateContainerReactHelpers();
const Demo: React.FC<{}> = () => {
const value = useSelector(selector);
return <>{value}>;
};
ReactDOM.render(
-
+
,
container
@@ -265,7 +253,7 @@ describe('hooks', () => {
});
test('re-renders when state changes', () => {
- const { store, mutators } = create({
+ const stateContainer = createStateContainer({
foo: {
bar: {
baz: 'qux',
@@ -280,7 +268,7 @@ describe('hooks', () => {
};
ReactDOM.render(
-
+
,
container
@@ -288,7 +276,7 @@ describe('hooks', () => {
expect(container!.innerHTML).toBe('qux');
act(() => {
- mutators.set({
+ stateContainer.set({
foo: {
bar: {
baz: 'quux',
@@ -300,9 +288,9 @@ describe('hooks', () => {
});
test("re-renders only when selector's result changes", async () => {
- const { store, mutators } = create({ a: 'b', foo: 'bar' });
+ const stateContainer = createStateContainer({ a: 'b', foo: 'bar' });
const selector = (state: { foo: string }) => state.foo;
- const { Provider, useSelector } = createStateContainerReactHelpers();
+ const { Provider, useSelector } = createStateContainerReactHelpers();
let cnt = 0;
const Demo: React.FC<{}> = () => {
@@ -311,7 +299,7 @@ describe('hooks', () => {
return <>{value}>;
};
ReactDOM.render(
-
+
,
container
@@ -321,14 +309,14 @@ describe('hooks', () => {
expect(cnt).toBe(1);
act(() => {
- mutators.set({ a: 'c', foo: 'bar' });
+ stateContainer.set({ a: 'c', foo: 'bar' });
});
await new Promise(r => setTimeout(r, 1));
expect(cnt).toBe(1);
act(() => {
- mutators.set({ a: 'd', foo: 'bar 2' });
+ stateContainer.set({ a: 'd', foo: 'bar 2' });
});
await new Promise(r => setTimeout(r, 1));
@@ -336,9 +324,9 @@ describe('hooks', () => {
});
test('does not re-render on same shape object', async () => {
- const { store, mutators } = create({ foo: { bar: 'baz' } });
+ const stateContainer = createStateContainer({ foo: { bar: 'baz' } });
const selector = (state: { foo: any }) => state.foo;
- const { Provider, useSelector } = createStateContainerReactHelpers();
+ const { Provider, useSelector } = createStateContainerReactHelpers();
let cnt = 0;
const Demo: React.FC<{}> = () => {
@@ -347,7 +335,7 @@ describe('hooks', () => {
return <>{JSON.stringify(value)}>;
};
ReactDOM.render(
-
+
,
container
@@ -357,14 +345,14 @@ describe('hooks', () => {
expect(cnt).toBe(1);
act(() => {
- mutators.set({ foo: { bar: 'baz' } });
+ stateContainer.set({ foo: { bar: 'baz' } });
});
await new Promise(r => setTimeout(r, 1));
expect(cnt).toBe(1);
act(() => {
- mutators.set({ foo: { bar: 'qux' } });
+ stateContainer.set({ foo: { bar: 'qux' } });
});
await new Promise(r => setTimeout(r, 1));
@@ -372,7 +360,7 @@ describe('hooks', () => {
});
test('can set custom comparator function to prevent re-renders on deep equality', async () => {
- const { store, mutators } = create(
+ const stateContainer = createStateContainer(
{ foo: { bar: 'baz' } },
{
set: () => (newState: { foo: { bar: string } }) => newState,
@@ -380,7 +368,7 @@ describe('hooks', () => {
);
const selector = (state: { foo: any }) => state.foo;
const comparator = (prev: any, curr: any) => JSON.stringify(prev) === JSON.stringify(curr);
- const { Provider, useSelector } = createStateContainerReactHelpers();
+ const { Provider, useSelector } = createStateContainerReactHelpers();
let cnt = 0;
const Demo: React.FC<{}> = () => {
@@ -389,7 +377,7 @@ describe('hooks', () => {
return <>{JSON.stringify(value)}>;
};
ReactDOM.render(
-
+
,
container
@@ -399,13 +387,13 @@ describe('hooks', () => {
expect(cnt).toBe(1);
act(() => {
- mutators.set({ foo: { bar: 'baz' } });
+ stateContainer.set({ foo: { bar: 'baz' } });
});
await new Promise(r => setTimeout(r, 1));
expect(cnt).toBe(1);
});
- xtest('unsubscribes when React un-mounts', () => {});
+ test.todo('unsubscribes when React un-mounts');
});
});
diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts
index 45b34b13251f4..36903f2d7c90f 100644
--- a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts
+++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts
@@ -35,7 +35,7 @@ export const createStateContainerReactHelpers = useContainer().transitions;
+ const useTransitions: () => Container['transitions'] = () => useContainer().transitions;
const useSelector = (
selector: (state: UnboxState) => Result,
diff --git a/src/plugins/kibana_utils/public/state_containers/types.ts b/src/plugins/kibana_utils/public/state_containers/types.ts
index e120f60e72b8f..5f27a3d2c1dca 100644
--- a/src/plugins/kibana_utils/public/state_containers/types.ts
+++ b/src/plugins/kibana_utils/public/state_containers/types.ts
@@ -20,12 +20,13 @@
import { Observable } from 'rxjs';
import { Ensure, RecursiveReadonly } from '@kbn/utility-types';
+export type BaseState = object;
export interface TransitionDescription {
type: Type;
args: Args;
}
-export type Transition = (...args: Args) => State;
-export type PureTransition = (
+export type Transition = (...args: Args) => State;
+export type PureTransition = (
state: RecursiveReadonly
) => Transition;
export type EnsurePureTransition = Ensure>;
@@ -34,15 +35,15 @@ export type PureTransitionsToTransitions = {
[K in keyof T]: PureTransitionToTransition>;
};
-export interface BaseStateContainer {
+export interface BaseStateContainer {
get: () => RecursiveReadonly;
set: (state: State) => void;
state$: Observable>;
}
export interface StateContainer<
- State,
- PureTransitions extends object = {},
+ State extends BaseState,
+ PureTransitions extends object,
PureSelectors extends object = {}
> extends BaseStateContainer {
transitions: Readonly>;
@@ -50,7 +51,7 @@ export interface StateContainer<
}
export interface ReduxLikeStateContainer<
- State,
+ State extends BaseState,
PureTransitions extends object = {},
PureSelectors extends object = {}
> extends StateContainer {
@@ -63,14 +64,16 @@ export interface ReduxLikeStateContainer<
}
export type Dispatch = (action: T) => void;
-
-export type Middleware = (
+export type Middleware = (
store: Pick, 'getState' | 'dispatch'>
) => (
next: (action: TransitionDescription) => TransitionDescription | any
) => Dispatch;
-export type Reducer = (state: State, action: TransitionDescription) => State;
+export type Reducer = (
+ state: State,
+ action: TransitionDescription
+) => State;
export type UnboxState<
Container extends StateContainer
@@ -80,7 +83,7 @@ export type UnboxTransitions<
> = Container extends StateContainer ? T : never;
export type Selector = (...args: Args) => Result;
-export type PureSelector = (
+export type PureSelector = (
state: State
) => Selector;
export type EnsurePureSelector = Ensure>;
@@ -93,7 +96,12 @@ export type PureSelectorsToSelectors = {
export type Comparator = (previous: Result, current: Result) => boolean;
-export type MapStateToProps = (state: State) => StateProps;
-export type Connect = (
+export type MapStateToProps = (
+ state: State
+) => StateProps;
+export type Connect = <
+ Props extends object,
+ StatePropKeys extends keyof Props
+>(
mapStateToProp: MapStateToProps>
) => (component: React.ComponentType) => React.FC>;
diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts
index cc513bc674d0f..08ad1551420d2 100644
--- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts
+++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { BaseStateContainer, createStateContainer } from '../state_containers';
+import { BaseState, BaseStateContainer, createStateContainer } from '../state_containers';
import {
defaultState,
pureTransitions,
@@ -89,7 +89,7 @@ describe('state_sync', () => {
// initial sync of storage to state is not happening
expect(container.getState()).toEqual(defaultState);
- const storageState2 = [{ id: 1, text: 'todo', completed: true }];
+ const storageState2 = { todos: [{ id: 1, text: 'todo', completed: true }] };
(testStateStorage.get as jest.Mock).mockImplementation(() => storageState2);
storageChange$.next(storageState2);
@@ -124,7 +124,7 @@ describe('state_sync', () => {
start();
const originalState = container.getState();
- const storageState = [...originalState];
+ const storageState = { ...originalState };
(testStateStorage.get as jest.Mock).mockImplementation(() => storageState);
storageChange$.next(storageState);
@@ -134,7 +134,7 @@ describe('state_sync', () => {
});
it('storage change to null should notify state', () => {
- container.set([{ completed: false, id: 1, text: 'changed' }]);
+ container.set({ todos: [{ completed: false, id: 1, text: 'changed' }] });
const { stop, start } = syncStates([
{
stateContainer: withDefaultState(container, defaultState),
@@ -189,8 +189,8 @@ describe('state_sync', () => {
]);
start();
- const newStateFromUrl = [{ completed: false, id: 1, text: 'changed' }];
- history.replace('/#?_s=!((completed:!f,id:1,text:changed))');
+ const newStateFromUrl = { todos: [{ completed: false, id: 1, text: 'changed' }] };
+ history.replace('/#?_s=(todos:!((completed:!f,id:1,text:changed)))');
expect(container.getState()).toEqual(newStateFromUrl);
expect(JSON.parse(sessionStorage.getItem(key)!)).toEqual(newStateFromUrl);
@@ -220,7 +220,7 @@ describe('state_sync', () => {
expect(history.length).toBe(startHistoryLength + 1);
expect(getCurrentUrl()).toMatchInlineSnapshot(
- `"/#?_s=!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3'))"`
+ `"/#?_s=(todos:!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3')))"`
);
stop();
@@ -248,14 +248,14 @@ describe('state_sync', () => {
expect(history.length).toBe(startHistoryLength + 1);
expect(getCurrentUrl()).toMatchInlineSnapshot(
- `"/#?_s=!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3'))"`
+ `"/#?_s=(todos:!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3')))"`
);
await tick();
expect(history.length).toBe(startHistoryLength + 1);
expect(getCurrentUrl()).toMatchInlineSnapshot(
- `"/#?_s=!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3'))"`
+ `"/#?_s=(todos:!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3')))"`
);
stop();
@@ -294,7 +294,7 @@ describe('state_sync', () => {
});
});
-function withDefaultState(
+function withDefaultState(
stateContainer: BaseStateContainer,
// eslint-disable-next-line no-shadow
defaultState: State
@@ -302,7 +302,10 @@ function withDefaultState(
return {
...stateContainer,
set: (state: State | null) => {
- stateContainer.set(state || defaultState);
+ stateContainer.set({
+ ...defaultState,
+ ...state,
+ });
},
};
}
diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts
index f0ef1423dec71..9c1116e5da531 100644
--- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts
+++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts
@@ -23,6 +23,7 @@ import defaultComparator from 'fast-deep-equal';
import { IStateSyncConfig } from './types';
import { IStateStorage } from './state_sync_state_storage';
import { distinctUntilChangedWithInitialValue } from '../../common';
+import { BaseState } from '../state_containers';
/**
* Utility for syncing application state wrapped in state container
@@ -86,7 +87,10 @@ export interface ISyncStateRef({
+export function syncState<
+ State extends BaseState,
+ StateStorage extends IStateStorage = IStateStorage
+>({
storageKey,
stateStorage,
stateContainer,
diff --git a/src/plugins/kibana_utils/public/state_sync/types.ts b/src/plugins/kibana_utils/public/state_sync/types.ts
index 0f7395ad0f0e5..3009c1d161a53 100644
--- a/src/plugins/kibana_utils/public/state_sync/types.ts
+++ b/src/plugins/kibana_utils/public/state_sync/types.ts
@@ -17,10 +17,11 @@
* under the License.
*/
-import { BaseStateContainer } from '../state_containers/types';
+import { BaseState, BaseStateContainer } from '../state_containers/types';
import { IStateStorage } from './state_sync_state_storage';
-export interface INullableBaseStateContainer extends BaseStateContainer {
+export interface INullableBaseStateContainer
+ extends BaseStateContainer {
// State container for stateSync() have to accept "null"
// for example, set() implementation could handle null and fallback to some default state
// this is required to handle edge case, when state in storage becomes empty and syncing is in progress.
@@ -29,7 +30,7 @@ export interface INullableBaseStateContainer extends BaseStateContainer {
/**
diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json
index f9a368e85ed49..d77f4ac92da16 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 599726612a936..dfb2394af237b 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 f508aa250454f..dcd0cd500bbc3 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 0000000000000..140f61fa2f3fd
--- /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 0000000000000..136f5b9e5cfad
--- /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 0acd452530b30..ec37277cae0f8 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/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts
index 38ee5b7db39c4..e25d295515971 100644
--- a/test/accessibility/apps/discover.ts
+++ b/test/accessibility/apps/discover.ts
@@ -20,10 +20,12 @@
import { FtrProviderContext } from '../ftr_provider_context';
export default function({ getService, getPageObjects }: FtrProviderContext) {
- const PageObjects = getPageObjects(['common', 'timePicker']);
+ const PageObjects = getPageObjects(['common', 'discover', 'header', 'share', 'timePicker']);
const a11y = getService('a11y');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
+ const inspector = getService('inspector');
+ const filterBar = getService('filterBar');
describe('Discover', () => {
before(async () => {
@@ -39,5 +41,73 @@ export default function({ getService, getPageObjects }: FtrProviderContext) {
it('main view', async () => {
await a11y.testAppSnapshot();
});
+
+ it('Click save button', async () => {
+ await PageObjects.discover.clickSaveSearchButton();
+ await a11y.testAppSnapshot();
+ });
+
+ it('Save search panel', async () => {
+ await PageObjects.discover.inputSavedSearchTitle('a11ySearch');
+ await a11y.testAppSnapshot();
+ });
+
+ it('Confirm saved search', async () => {
+ await PageObjects.discover.clickConfirmSavedSearch();
+ await a11y.testAppSnapshot();
+ });
+
+ // skipping the test for new because we can't fix it right now
+ it.skip('Click on new to clear the search', async () => {
+ await PageObjects.discover.clickNewSearchButton();
+ await a11y.testAppSnapshot();
+ });
+
+ it('Open load saved search panel', async () => {
+ await PageObjects.discover.openLoadSavedSearchPanel();
+ await a11y.testAppSnapshot();
+ await PageObjects.discover.closeLoadSavedSearchPanel();
+ });
+
+ it('Open inspector panel', async () => {
+ await inspector.open();
+ await a11y.testAppSnapshot();
+ await inspector.close();
+ });
+
+ it('Open add filter', async () => {
+ await PageObjects.discover.openAddFilterPanel();
+ await a11y.testAppSnapshot();
+ });
+
+ it('Select values for a filter', async () => {
+ await filterBar.addFilter('extension.raw', 'is one of', 'jpg');
+ await a11y.testAppSnapshot();
+ });
+
+ it('Load a new search from the panel', async () => {
+ await PageObjects.discover.clickSaveSearchButton();
+ await PageObjects.discover.inputSavedSearchTitle('filterSearch');
+ await PageObjects.discover.clickConfirmSavedSearch();
+ await PageObjects.discover.openLoadSavedSearchPanel();
+ await PageObjects.discover.loadSavedSearch('filterSearch');
+ await a11y.testAppSnapshot();
+ });
+
+ // unable to validate on EUI pop-over
+ it('click share button', async () => {
+ await PageObjects.share.clickShareTopNavButton();
+ await a11y.testAppSnapshot();
+ });
+
+ it('Open sidebar filter', async () => {
+ await PageObjects.discover.openSidebarFieldFilter();
+ await a11y.testAppSnapshot();
+ });
+
+ it('Close sidebar filter', async () => {
+ await PageObjects.discover.closeSidebarFieldFilter();
+ await a11y.testAppSnapshot();
+ });
});
}
diff --git a/test/accessibility/services/a11y/a11y.ts b/test/accessibility/services/a11y/a11y.ts
index 7adfe7ebfcc7d..72440b648e538 100644
--- a/test/accessibility/services/a11y/a11y.ts
+++ b/test/accessibility/services/a11y/a11y.ts
@@ -45,7 +45,6 @@ export const normalizeResult = (report: any) => {
export function A11yProvider({ getService }: FtrProviderContext) {
const browser = getService('browser');
const Wd = getService('__webdriver__');
- const log = getService('log');
/**
* Accessibility testing service using the Axe (https://www.deque.com/axe/)
@@ -78,11 +77,6 @@ export function A11yProvider({ getService }: FtrProviderContext) {
private testAxeReport(report: AxeReport) {
const errorMsgs = [];
- for (const result of report.incomplete) {
- // these items require human review and can't be definitively validated
- log.warning(printResult(chalk.yellow('UNABLE TO VALIDATE'), result));
- }
-
for (const result of report.violations) {
errorMsgs.push(printResult(chalk.red('VIOLATION'), result));
}
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 0000000000000..3901fa936e719
--- /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 9e1b04ad45874..c3c938c623731 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 2a955a2dc90b1..f5485c1db206e 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 5dcb18374c51f..6d2a30fa85325 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 bd31bb010f260..1cb9f1490d442 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 179f10223afb2..e3e835109da2c 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 23bb784c79cd0..2ea1389b89ad4 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 9900881e4690d..3a09b46a713cc 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 3b9e404e9b94d..b9172990c501d 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 0b73bc224ab74..b99de9fee6db1 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 683f3683e65e5..f30f58913bd97 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 212044d898251..a0b972f3ab63c 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 4aa862a4a0384..8088b5a0f9da9 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/fixtures/es_archiver/date_nanos_custom/data.json b/test/functional/fixtures/es_archiver/date_nanos_custom/data.json
new file mode 100644
index 0000000000000..73cba70a8b93d
--- /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 0000000000000..98af509c4e6f2
--- /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 b0f1a3304a9b8..af0a0160a81d8 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/discover_page.js b/test/functional/page_objects/discover_page.js
index 3ba0f217813f2..85d8cff675f2d 100644
--- a/test/functional/page_objects/discover_page.js
+++ b/test/functional/page_objects/discover_page.js
@@ -63,6 +63,18 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
});
}
+ async inputSavedSearchTitle(searchName) {
+ await testSubjects.setValue('savedObjectTitle', searchName);
+ }
+
+ async clickConfirmSavedSearch() {
+ await testSubjects.click('confirmSaveSavedObjectButton');
+ }
+
+ async openAddFilterPanel() {
+ await testSubjects.click('addFilter');
+ }
+
async waitUntilSearchingHasFinished() {
const spinner = await testSubjects.find('loadingSpinner');
await find.waitForElementHidden(spinner, defaultFindTimeout * 10);
@@ -117,6 +129,10 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
await testSubjects.click('discoverOpenButton');
}
+ async closeLoadSavedSearchPanel() {
+ await testSubjects.click('euiFlyoutCloseButton');
+ }
+
async getChartCanvas() {
return await find.byCssSelector('.echChart canvas:last-of-type');
}
diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts
index cf9eb4332c3e1..a641fbda023c3 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 5526243ea2bbd..4ba8ddb035913 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 8717517f44864..7c67678429478 100644
--- a/test/functional/page_objects/time_picker.js
+++ b/test/functional/page_objects/time_picker.js
@@ -264,6 +264,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 138e5758ede7c..0f14489a39dbc 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 7e512975356f3..30e13d551fa28 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 1562cf9745f2d..0071b8d993f70 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() {
@@ -322,6 +314,10 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide
async () => (await globalNav.getLastBreadcrumb()) === vizName
);
}
+
+ public async clickLensWidget() {
+ await this.clickVisType('lens');
+ }
}
return new VisualizePage();
diff --git a/test/functional/services/apps_menu.ts b/test/functional/services/apps_menu.ts
index a4cd98b2a06ec..fe17532f6a41a 100644
--- a/test/functional/services/apps_menu.ts
+++ b/test/functional/services/apps_menu.ts
@@ -25,7 +25,7 @@ export function AppsMenuProvider({ getService }: FtrProviderContext) {
return new (class AppsMenu {
/**
- * Get the text and href from each of the links in the apps menu
+ * Get the attributes from each of the links in the apps menu
*/
public async readLinks() {
const appMenu = await testSubjects.find('navDrawer');
@@ -37,12 +37,21 @@ export function AppsMenuProvider({ getService }: FtrProviderContext) {
return {
text: $(link).text(),
href: $(link).attr('href'),
+ disabled: $(link).attr('disabled') != null,
};
});
return links;
}
+ /**
+ * Get the attributes from the link with the given name.
+ * @param name
+ */
+ public async getLink(name: string) {
+ return (await this.readLinks()).find(nl => nl.text === name);
+ }
+
/**
* Determine if an app link with the given name exists
* @param name
diff --git a/test/functional/services/dashboard/visualizations.js b/test/functional/services/dashboard/visualizations.js
index 5e722ccce8970..f7a6fb7d2f694 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 ec886cf694f2e..c7667ae7b4049 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 02c507dbb3ed8..1eac93c8538e4 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.0.0",
"react": "^16.12.0",
"react-dom": "^16.12.0"
}
diff --git a/test/plugin_functional/plugins/core_app_status/kibana.json b/test/plugin_functional/plugins/core_app_status/kibana.json
new file mode 100644
index 0000000000000..91d8e6fd8f9e1
--- /dev/null
+++ b/test/plugin_functional/plugins/core_app_status/kibana.json
@@ -0,0 +1,8 @@
+{
+ "id": "core_app_status",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "configPath": ["core_app_status"],
+ "server": false,
+ "ui": true
+}
diff --git a/test/plugin_functional/plugins/core_app_status/package.json b/test/plugin_functional/plugins/core_app_status/package.json
new file mode 100644
index 0000000000000..61655487c6acb
--- /dev/null
+++ b/test/plugin_functional/plugins/core_app_status/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "core_app_status",
+ "version": "1.0.0",
+ "main": "target/test/plugin_functional/plugins/core_app_status",
+ "kibana": {
+ "version": "kibana",
+ "templateVersion": "1.0.0"
+ },
+ "license": "Apache-2.0",
+ "scripts": {
+ "kbn": "node ../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && tsc"
+ },
+ "devDependencies": {
+ "typescript": "3.5.3"
+ }
+}
diff --git a/test/plugin_functional/plugins/core_app_status/public/application.tsx b/test/plugin_functional/plugins/core_app_status/public/application.tsx
new file mode 100644
index 0000000000000..323774392a6d7
--- /dev/null
+++ b/test/plugin_functional/plugins/core_app_status/public/application.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 { render, unmountComponentAtNode } from 'react-dom';
+import {
+ EuiPage,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentBody,
+ EuiPageContentHeader,
+ EuiPageContentHeaderSection,
+ EuiPageHeader,
+ EuiPageHeaderSection,
+ EuiTitle,
+} from '@elastic/eui';
+
+import { AppMountContext, AppMountParameters } from 'kibana/public';
+
+const AppStatusApp = () => (
+
+
+
+
+
+ Welcome to App Status Test App!
+
+
+
+
+
+
+
+ App Status Test App home page section title
+
+
+
+ App Status Test App content
+
+
+
+);
+
+export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => {
+ render( , element);
+
+ return () => unmountComponentAtNode(element);
+};
diff --git a/test/plugin_functional/plugins/core_app_status/public/index.ts b/test/plugin_functional/plugins/core_app_status/public/index.ts
new file mode 100644
index 0000000000000..e0ad7c25a54b8
--- /dev/null
+++ b/test/plugin_functional/plugins/core_app_status/public/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { PluginInitializer } from 'kibana/public';
+import { CoreAppStatusPlugin, CoreAppStatusPluginSetup, CoreAppStatusPluginStart } from './plugin';
+
+export const plugin: PluginInitializer = () =>
+ new CoreAppStatusPlugin();
diff --git a/test/plugin_functional/plugins/core_app_status/public/plugin.tsx b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx
new file mode 100644
index 0000000000000..85caaaf5f9090
--- /dev/null
+++ b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 { Plugin, CoreSetup, AppUpdater, AppUpdatableFields, CoreStart } from 'kibana/public';
+import { BehaviorSubject } from 'rxjs';
+
+export class CoreAppStatusPlugin
+ implements Plugin {
+ private appUpdater = new BehaviorSubject(() => ({}));
+
+ public setup(core: CoreSetup, deps: {}) {
+ core.application.register({
+ id: 'app_status',
+ title: 'App Status',
+ euiIconType: 'snowflake',
+ updater$: this.appUpdater,
+ async mount(context, params) {
+ const { renderApp } = await import('./application');
+ return renderApp(context, params);
+ },
+ });
+
+ return {};
+ }
+
+ public start(core: CoreStart) {
+ return {
+ setAppStatus: (status: Partial) => {
+ this.appUpdater.next(() => status);
+ },
+ navigateToApp: async (appId: string) => {
+ return core.application.navigateToApp(appId);
+ },
+ };
+ }
+ public stop() {}
+}
+
+export type CoreAppStatusPluginSetup = ReturnType;
+export type CoreAppStatusPluginStart = ReturnType;
diff --git a/test/plugin_functional/plugins/core_app_status/tsconfig.json b/test/plugin_functional/plugins/core_app_status/tsconfig.json
new file mode 100644
index 0000000000000..5fcaeafbb0d85
--- /dev/null
+++ b/test/plugin_functional/plugins/core_app_status/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true
+ },
+ "include": [
+ "index.ts",
+ "public/**/*.ts",
+ "public/**/*.tsx",
+ "../../../../typings/**/*",
+ ],
+ "exclude": []
+}
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 67ad28c083dbc..1bfb1e8ba4bca 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.0.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 b22a1ff2d4176..6d6b04fba889c 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.0.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 8c91826d7b450..964adacb2ac09 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.0.0",
"react": "^16.12.0"
},
"scripts": {
diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts
new file mode 100644
index 0000000000000..703ae30533bae
--- /dev/null
+++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts
@@ -0,0 +1,116 @@
+/*
+ * 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 {
+ AppNavLinkStatus,
+ AppStatus,
+ AppUpdatableFields,
+} from '../../../../src/core/public/application/types';
+import { PluginFunctionalProviderContext } from '../../services';
+import { CoreAppStatusPluginStart } from '../../plugins/core_app_status/public/plugin';
+import '../../plugins/core_provider_plugin/types';
+
+// eslint-disable-next-line import/no-default-export
+export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) {
+ const PageObjects = getPageObjects(['common']);
+ const browser = getService('browser');
+ const appsMenu = getService('appsMenu');
+
+ const setAppStatus = async (s: Partial) => {
+ await browser.executeAsync(async (status: Partial, cb: Function) => {
+ const plugin = window.__coreProvider.start.plugins
+ .core_app_status as CoreAppStatusPluginStart;
+ plugin.setAppStatus(status);
+ cb();
+ }, s);
+ };
+
+ const navigateToApp = async (i: string): Promise<{ error?: string }> => {
+ return (await browser.executeAsync(async (appId, cb: Function) => {
+ // navigating in legacy mode performs a page refresh
+ // and webdriver seems to re-execute the script after the reload
+ // as it considers it didn't end on the previous session.
+ // however when testing navigation to NP app, __coreProvider is not accessible
+ // so we need to check for existence.
+ if (!window.__coreProvider) {
+ cb({});
+ }
+ const plugin = window.__coreProvider.start.plugins
+ .core_app_status as CoreAppStatusPluginStart;
+ try {
+ await plugin.navigateToApp(appId);
+ cb({});
+ } catch (e) {
+ cb({
+ error: e.message,
+ });
+ }
+ }, i)) as any;
+ };
+
+ describe('application status management', () => {
+ beforeEach(async () => {
+ await PageObjects.common.navigateToApp('settings');
+ });
+
+ it('can change the navLink status at runtime', async () => {
+ await setAppStatus({
+ navLinkStatus: AppNavLinkStatus.disabled,
+ });
+ let link = await appsMenu.getLink('App Status');
+ expect(link).not.to.eql(undefined);
+ expect(link!.disabled).to.eql(true);
+
+ await setAppStatus({
+ navLinkStatus: AppNavLinkStatus.hidden,
+ });
+ link = await appsMenu.getLink('App Status');
+ expect(link).to.eql(undefined);
+
+ await setAppStatus({
+ navLinkStatus: AppNavLinkStatus.visible,
+ tooltip: 'Some tooltip',
+ });
+ link = await appsMenu.getLink('Some tooltip'); // the tooltip replaces the name in the selector we use.
+ expect(link).not.to.eql(undefined);
+ expect(link!.disabled).to.eql(false);
+ });
+
+ it('shows an error when navigating to an inaccessible app', async () => {
+ await setAppStatus({
+ status: AppStatus.inaccessible,
+ });
+
+ const result = await navigateToApp('app_status');
+ expect(result.error).to.contain(
+ 'Trying to navigate to an inaccessible application: app_status'
+ );
+ });
+
+ it('allows to navigate to an accessible app', async () => {
+ await setAppStatus({
+ status: AppStatus.accessible,
+ });
+
+ const result = await navigateToApp('app_status');
+ expect(result.error).to.eql(undefined);
+ });
+ });
+}
diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts
index a3c9d9d63e353..231458fad155b 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/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts
index 6c55245d10f03..d66e2e7dc5da7 100644
--- a/test/plugin_functional/test_suites/core_plugins/index.ts
+++ b/test/plugin_functional/test_suites/core_plugins/index.ts
@@ -28,5 +28,6 @@ export default function({ loadTestFile }: PluginFunctionalProviderContext) {
loadTestFile(require.resolve('./ui_settings'));
loadTestFile(require.resolve('./top_nav'));
loadTestFile(require.resolve('./application_leave_confirm'));
+ loadTestFile(require.resolve('./application_status'));
});
}
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 7e86d2f1dc435..71e3bdd6c8c84 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 56547f89b1e90..83a7b5540334f 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/action_type_registry.test.ts b/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts
index 2f15ae1c0a2b3..63f1b545179c7 100644
--- a/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts
+++ b/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../plugins/task_manager/server/task_manager.mock';
import { ActionTypeRegistry } from './action_type_registry';
import { ExecutorType } from './types';
import { ActionExecutor, ExecutorError, TaskRunnerFactory } from './lib';
import { configUtilsMock } from './actions_config.mock';
-const mockTaskManager = taskManagerMock.create();
+const mockTaskManager = taskManagerMock.setup();
const actionTypeRegistryParams = {
taskManager: mockTaskManager,
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
diff --git a/x-pack/legacy/plugins/actions/server/action_type_registry.ts b/x-pack/legacy/plugins/actions/server/action_type_registry.ts
index f66d1947c2b8b..351c1add7b451 100644
--- a/x-pack/legacy/plugins/actions/server/action_type_registry.ts
+++ b/x-pack/legacy/plugins/actions/server/action_type_registry.ts
@@ -6,11 +6,11 @@
import Boom from 'boom';
import { i18n } from '@kbn/i18n';
-import { TaskManagerSetupContract } from './shim';
-import { RunContext } from '../../task_manager/server';
+import { RunContext, TaskManagerSetupContract } from '../../../../plugins/task_manager/server';
import { ExecutorError, TaskRunnerFactory } from './lib';
import { ActionType } from './types';
import { ActionsConfigurationUtilities } from './actions_config';
+
interface ConstructorOptions {
taskManager: TaskManagerSetupContract;
taskRunnerFactory: TaskRunnerFactory;
diff --git a/x-pack/legacy/plugins/actions/server/actions_client.test.ts b/x-pack/legacy/plugins/actions/server/actions_client.test.ts
index 9e75248c56cae..dfbd2db4b6842 100644
--- a/x-pack/legacy/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/legacy/plugins/actions/server/actions_client.test.ts
@@ -10,7 +10,7 @@ import { ActionTypeRegistry } from './action_type_registry';
import { ActionsClient } from './actions_client';
import { ExecutorType } from './types';
import { ActionExecutor, TaskRunnerFactory } from './lib';
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../plugins/task_manager/server/task_manager.mock';
import { configUtilsMock } from './actions_config.mock';
import { getActionsConfigurationUtilities } from './actions_config';
@@ -23,7 +23,7 @@ const defaultKibanaIndex = '.kibana';
const savedObjectsClient = savedObjectsClientMock.create();
const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
-const mockTaskManager = taskManagerMock.create();
+const mockTaskManager = taskManagerMock.setup();
const actionTypeRegistryParams = {
taskManager: mockTaskManager,
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 4aaecc8e9d7df..74263c603c11e 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 dd2bd328ce53f..94d7852e76fad 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 1da8b06e1587a..dbac84ef681f1 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 0e9fe0483ee1e..ddf33ba63f71a 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/index.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/index.test.ts
index 3a0c9f415cc2b..5fcf39c2e8fdd 100644
--- a/x-pack/legacy/plugins/actions/server/builtin_action_types/index.test.ts
+++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/index.test.ts
@@ -6,7 +6,7 @@
import { ActionExecutor, TaskRunnerFactory } from '../lib';
import { ActionTypeRegistry } from '../action_type_registry';
-import { taskManagerMock } from '../../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../../plugins/task_manager/server/task_manager.mock';
import { registerBuiltInActionTypes } from './index';
import { Logger } from '../../../../../../src/core/server';
import { loggingServiceMock } from '../../../../../../src/core/server/mocks';
@@ -20,7 +20,7 @@ export function createActionTypeRegistry(): {
} {
const logger = loggingServiceMock.create().get() as jest.Mocked;
const actionTypeRegistry = new ActionTypeRegistry({
- taskManager: taskManagerMock.create(),
+ taskManager: taskManagerMock.setup(),
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
actionsConfigUtils: configUtilsMock,
});
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 cb3548524ebbb..f60fdf7fef95e 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 250c169278c57..b26621702cf5b 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 c59ddf97017fd..8f28b9e8f5125 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 0edf409e4d46c..34b8602eeba36 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 a2b0db8bdb70f..aebc9c4993599 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 92611d6f162ff..b8989e59a2257 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 64dd3a485f8e2..b95fef97ac7b9 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 06fe2fb0e591c..fa88d3c72c163 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/actions/server/create_execute_function.test.ts b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts
index 6de446ee2da76..7dbcfce5ee335 100644
--- a/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts
+++ b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts
@@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../plugins/task_manager/server/task_manager.mock';
import { createExecuteFunction } from './create_execute_function';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
-const mockTaskManager = taskManagerMock.create();
+const mockTaskManager = taskManagerMock.start();
const savedObjectsClient = savedObjectsClientMock.create();
const getBasePath = jest.fn();
diff --git a/x-pack/legacy/plugins/actions/server/create_execute_function.ts b/x-pack/legacy/plugins/actions/server/create_execute_function.ts
index 8ff12b8c3fa4b..ddd8b1df2327b 100644
--- a/x-pack/legacy/plugins/actions/server/create_execute_function.ts
+++ b/x-pack/legacy/plugins/actions/server/create_execute_function.ts
@@ -5,7 +5,7 @@
*/
import { SavedObjectsClientContract } from 'src/core/server';
-import { TaskManagerStartContract } from './shim';
+import { TaskManagerStartContract } from '../../../../plugins/task_manager/server';
import { GetBasePathFunction } from './types';
interface CreateExecuteFunctionOptions {
diff --git a/x-pack/legacy/plugins/actions/server/init.ts b/x-pack/legacy/plugins/actions/server/init.ts
index 5eab3418467bc..6f221b08c4bc5 100644
--- a/x-pack/legacy/plugins/actions/server/init.ts
+++ b/x-pack/legacy/plugins/actions/server/init.ts
@@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { Legacy } from 'kibana';
import { Plugin } from './plugin';
-import { shim, Server } from './shim';
+import { shim } from './shim';
import { ActionsPlugin } from './types';
-export async function init(server: Server) {
+export async function init(server: Legacy.Server) {
const { initializerContext, coreSetup, coreStart, pluginsSetup, pluginsStart } = shim(server);
const plugin = new Plugin(initializerContext);
diff --git a/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts
index 5b60696c42d52..ad2b74da0d7d4 100644
--- a/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts
+++ b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts
@@ -7,7 +7,7 @@
import sinon from 'sinon';
import { ExecutorError } from './executor_error';
import { ActionExecutor } from './action_executor';
-import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server';
+import { ConcreteTaskInstance, TaskStatus } from '../../../../../plugins/task_manager/server';
import { TaskRunnerFactory } from './task_runner_factory';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { actionExecutorMock } from './action_executor.mock';
diff --git a/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.ts
index ca6a726f40e14..2dc3d1161399e 100644
--- a/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.ts
+++ b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.ts
@@ -6,7 +6,7 @@
import { ActionExecutorContract } from './action_executor';
import { ExecutorError } from './executor_error';
-import { RunContext } from '../../../task_manager/server';
+import { RunContext } from '../../../../../plugins/task_manager/server';
import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server';
import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types';
diff --git a/x-pack/legacy/plugins/actions/server/plugin.ts b/x-pack/legacy/plugins/actions/server/plugin.ts
index 48f99ba5135b7..ffc4a9cf90e54 100644
--- a/x-pack/legacy/plugins/actions/server/plugin.ts
+++ b/x-pack/legacy/plugins/actions/server/plugin.ts
@@ -93,7 +93,7 @@ export class Plugin {
const actionsConfigUtils = getActionsConfigurationUtilities(config as ActionsConfigType);
const actionTypeRegistry = new ActionTypeRegistry({
taskRunnerFactory,
- taskManager: plugins.task_manager,
+ taskManager: plugins.taskManager,
actionsConfigUtils,
});
this.taskRunnerFactory = taskRunnerFactory;
@@ -164,7 +164,7 @@ export class Plugin {
});
const executeFn = createExecuteFunction({
- taskManager: plugins.task_manager,
+ taskManager: plugins.taskManager,
getScopedSavedObjectsClient: core.savedObjects.getScopedSavedObjectsClient,
getBasePath,
});
diff --git a/x-pack/legacy/plugins/actions/server/shim.ts b/x-pack/legacy/plugins/actions/server/shim.ts
index f8aa9b8d7a25c..8077dc67c92c4 100644
--- a/x-pack/legacy/plugins/actions/server/shim.ts
+++ b/x-pack/legacy/plugins/actions/server/shim.ts
@@ -8,7 +8,11 @@ import Hapi from 'hapi';
import { Legacy } from 'kibana';
import * as Rx from 'rxjs';
import { ActionsConfigType } from './types';
-import { TaskManager } from '../../task_manager/server';
+import {
+ TaskManagerStartContract,
+ TaskManagerSetupContract,
+} from '../../../../plugins/task_manager/server';
+import { getTaskManagerSetup, getTaskManagerStart } from '../../task_manager/server';
import { XPackMainPlugin } from '../../xpack_main/server/xpack_main';
import KbnServer from '../../../../../src/legacy/server/kbn_server';
import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces';
@@ -24,16 +28,6 @@ import {
} from '../../../../../src/core/server';
import { LicensingPluginSetup } from '../../../../plugins/licensing/server';
-// Extend PluginProperties to indicate which plugins are guaranteed to exist
-// due to being marked as dependencies
-interface Plugins extends Hapi.PluginProperties {
- task_manager: TaskManager;
-}
-
-export interface Server extends Legacy.Server {
- plugins: Plugins;
-}
-
export interface KibanaConfig {
index: string;
}
@@ -41,14 +35,9 @@ export interface KibanaConfig {
/**
* Shim what we're thinking setup and start contracts will look like
*/
-export type TaskManagerStartContract = Pick;
export type XPackMainPluginSetupContract = Pick;
export type SecurityPluginSetupContract = Pick;
export type SecurityPluginStartContract = Pick;
-export type TaskManagerSetupContract = Pick<
- TaskManager,
- 'addMiddleware' | 'registerTaskDefinitions'
->;
/**
* New platform interfaces
@@ -74,7 +63,7 @@ export interface ActionsCoreStart {
}
export interface ActionsPluginsSetup {
security?: SecurityPluginSetupContract;
- task_manager: TaskManagerSetupContract;
+ taskManager: TaskManagerSetupContract;
xpack_main: XPackMainPluginSetupContract;
encryptedSavedObjects: EncryptedSavedObjectsSetupContract;
licensing: LicensingPluginSetup;
@@ -83,7 +72,7 @@ export interface ActionsPluginsStart {
security?: SecurityPluginStartContract;
spaces: () => SpacesPluginStartContract | undefined;
encryptedSavedObjects: EncryptedSavedObjectsStartContract;
- task_manager: TaskManagerStartContract;
+ taskManager: TaskManagerStartContract;
}
/**
@@ -92,7 +81,7 @@ export interface ActionsPluginsStart {
* @param server Hapi server instance
*/
export function shim(
- server: Server
+ server: Legacy.Server
): {
initializerContext: ActionsPluginInitializerContext;
coreSetup: ActionsCoreSetup;
@@ -132,7 +121,7 @@ export function shim(
const pluginsSetup: ActionsPluginsSetup = {
security: newPlatform.setup.plugins.security as SecurityPluginSetupContract | undefined,
- task_manager: server.plugins.task_manager,
+ taskManager: getTaskManagerSetup(server)!,
xpack_main: server.plugins.xpack_main,
encryptedSavedObjects: newPlatform.setup.plugins
.encryptedSavedObjects as EncryptedSavedObjectsSetupContract,
@@ -146,7 +135,7 @@ export function shim(
spaces: () => server.plugins.spaces,
encryptedSavedObjects: newPlatform.start.plugins
.encryptedSavedObjects as EncryptedSavedObjectsStartContract,
- task_manager: server.plugins.task_manager,
+ taskManager: getTaskManagerStart(server)!,
};
return {
diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts
index 8e96ad8dae31c..e1a05d6460e25 100644
--- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts
+++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts
@@ -6,10 +6,9 @@
import { TaskRunnerFactory } from './task_runner';
import { AlertTypeRegistry } from './alert_type_registry';
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
-
-const taskManager = taskManagerMock.create();
+import { taskManagerMock } from '../../../../plugins/task_manager/server/task_manager.mock';
+const taskManager = taskManagerMock.setup();
const alertTypeRegistryParams = {
taskManager,
taskRunnerFactory: new TaskRunnerFactory(),
diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts
index 2003e810a05b5..1e9007202c452 100644
--- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts
+++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts
@@ -6,9 +6,8 @@
import Boom from 'boom';
import { i18n } from '@kbn/i18n';
+import { RunContext, TaskManagerSetupContract } from '../../../../plugins/task_manager/server';
import { TaskRunnerFactory } from './task_runner';
-import { RunContext } from '../../task_manager';
-import { TaskManagerSetupContract } from './shim';
import { AlertType } from './types';
interface ConstructorOptions {
diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts
index 32293d9755a2a..2af66059d9fed 100644
--- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts
+++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts
@@ -7,14 +7,14 @@ import uuid from 'uuid';
import { schema } from '@kbn/config-schema';
import { AlertsClient } from './alerts_client';
import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks';
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../plugins/task_manager/server/task_manager.mock';
import { alertTypeRegistryMock } from './alert_type_registry.mock';
-import { TaskStatus } from '../../task_manager/server';
+import { TaskStatus } from '../../../../plugins/task_manager/server';
import { IntervalSchedule } from './types';
import { resolvable } from './test_utils';
import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks';
-const taskManager = taskManagerMock.create();
+const taskManager = taskManagerMock.start();
const alertTypeRegistry = alertTypeRegistryMock.create();
const savedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createStart();
diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts
index 33a6b716e9b8a..fe96a233b8663 100644
--- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts
+++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts
@@ -22,7 +22,6 @@ import {
AlertType,
IntervalSchedule,
} from './types';
-import { TaskManagerStartContract } from './shim';
import { validateAlertTypeParams } from './lib';
import {
InvalidateAPIKeyParams,
@@ -30,6 +29,7 @@ import {
InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult,
} from '../../../../plugins/security/server';
import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server';
+import { TaskManagerStartContract } from '../../../../plugins/task_manager/server';
type NormalizedAlertAction = Omit;
export type CreateAPIKeyResult =
diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.test.ts
index 519001d07e089..754e02a3f1e5e 100644
--- a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.test.ts
+++ b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.test.ts
@@ -7,7 +7,7 @@
import { Request } from 'hapi';
import { AlertsClientFactory, ConstructorOpts } from './alerts_client_factory';
import { alertTypeRegistryMock } from './alert_type_registry.mock';
-import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { taskManagerMock } from '../../../../plugins/task_manager/server/task_manager.mock';
import { KibanaRequest } from '../../../../../src/core/server';
import { loggingServiceMock } from '../../../../../src/core/server/mocks';
import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks';
@@ -23,7 +23,7 @@ const securityPluginSetup = {
};
const alertsClientFactoryParams: jest.Mocked = {
logger: loggingServiceMock.create().get(),
- taskManager: taskManagerMock.create(),
+ taskManager: taskManagerMock.start(),
alertTypeRegistry: alertTypeRegistryMock.create(),
getSpaceId: jest.fn(),
spaceIdToNamespace: jest.fn(),
diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts
index 94a396fbaa806..eab1cc3ce627b 100644
--- a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts
+++ b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts
@@ -8,10 +8,11 @@ import Hapi from 'hapi';
import uuid from 'uuid';
import { AlertsClient } from './alerts_client';
import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types';
-import { SecurityPluginStartContract, TaskManagerStartContract } from './shim';
+import { SecurityPluginStartContract } from './shim';
import { KibanaRequest, Logger } from '../../../../../src/core/server';
import { InvalidateAPIKeyParams } from '../../../../plugins/security/server';
import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server';
+import { TaskManagerStartContract } from '../../../../plugins/task_manager/server';
export interface ConstructorOpts {
logger: Logger;
diff --git a/x-pack/legacy/plugins/alerting/server/lib/result_type.ts b/x-pack/legacy/plugins/alerting/server/lib/result_type.ts
index 644ae51292249..52843f6362303 100644
--- a/x-pack/legacy/plugins/alerting/server/lib/result_type.ts
+++ b/x-pack/legacy/plugins/alerting/server/lib/result_type.ts
@@ -15,6 +15,10 @@ export interface Err {
}
export type Result = Ok | Err;
+export type Resultable = {
+ [P in keyof T]: Result;
+};
+
export function asOk(value: T): Ok {
return {
tag: 'ok',
@@ -52,3 +56,7 @@ export function map(
): Resolution {
return isOk(result) ? onOk(result.value) : onErr(result.error);
}
+
+export function resolveErr(result: Result, onErr: (error: E) => T): T {
+ return isOk(result) ? result.value : onErr(result.error);
+}
diff --git a/x-pack/legacy/plugins/alerting/server/plugin.ts b/x-pack/legacy/plugins/alerting/server/plugin.ts
index fb16f579d4c70..357db9e3df97e 100644
--- a/x-pack/legacy/plugins/alerting/server/plugin.ts
+++ b/x-pack/legacy/plugins/alerting/server/plugin.ts
@@ -79,7 +79,7 @@ export class Plugin {
});
const alertTypeRegistry = new AlertTypeRegistry({
- taskManager: plugins.task_manager,
+ taskManager: plugins.taskManager,
taskRunnerFactory: this.taskRunnerFactory,
});
this.alertTypeRegistry = alertTypeRegistry;
@@ -116,7 +116,7 @@ export class Plugin {
const alertsClientFactory = new AlertsClientFactory({
alertTypeRegistry: this.alertTypeRegistry!,
logger: this.logger,
- taskManager: plugins.task_manager,
+ taskManager: plugins.taskManager,
securityPluginSetup: plugins.security,
encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects,
spaceIdToNamespace,
diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts
index ae29048d83dd9..ccc10f929e123 100644
--- a/x-pack/legacy/plugins/alerting/server/shim.ts
+++ b/x-pack/legacy/plugins/alerting/server/shim.ts
@@ -7,7 +7,11 @@
import Hapi from 'hapi';
import { Legacy } from 'kibana';
import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces';
-import { TaskManager } from '../../task_manager/server';
+import {
+ TaskManagerStartContract,
+ TaskManagerSetupContract,
+} from '../../../../plugins/task_manager/server';
+import { getTaskManagerSetup, getTaskManagerStart } from '../../task_manager/server';
import { XPackMainPlugin } from '../../xpack_main/server/xpack_main';
import KbnServer from '../../../../../src/legacy/server/kbn_server';
import {
@@ -31,7 +35,6 @@ import { LicensingPluginSetup } from '../../../../plugins/licensing/server';
// due to being marked as dependencies
interface Plugins extends Hapi.PluginProperties {
actions: ActionsPlugin;
- task_manager: TaskManager;
}
export interface Server extends Legacy.Server {
@@ -41,17 +44,9 @@ export interface Server extends Legacy.Server {
/**
* Shim what we're thinking setup and start contracts will look like
*/
-export type TaskManagerStartContract = Pick<
- TaskManager,
- 'schedule' | 'fetch' | 'remove' | 'runNow'
->;
export type SecurityPluginSetupContract = Pick;
export type SecurityPluginStartContract = Pick;
export type XPackMainPluginSetupContract = Pick;
-export type TaskManagerSetupContract = Pick<
- TaskManager,
- 'addMiddleware' | 'registerTaskDefinitions'
->;
/**
* New platform interfaces
@@ -73,7 +68,7 @@ export interface AlertingCoreStart {
}
export interface AlertingPluginsSetup {
security?: SecurityPluginSetupContract;
- task_manager: TaskManagerSetupContract;
+ taskManager: TaskManagerSetupContract;
actions: ActionsPluginSetupContract;
xpack_main: XPackMainPluginSetupContract;
encryptedSavedObjects: EncryptedSavedObjectsSetupContract;
@@ -84,7 +79,7 @@ export interface AlertingPluginsStart {
security?: SecurityPluginStartContract;
spaces: () => SpacesPluginStartContract | undefined;
encryptedSavedObjects: EncryptedSavedObjectsStartContract;
- task_manager: TaskManagerStartContract;
+ taskManager: TaskManagerStartContract;
}
/**
@@ -121,7 +116,7 @@ export function shim(
const pluginsSetup: AlertingPluginsSetup = {
security: newPlatform.setup.plugins.security as SecurityPluginSetupContract | undefined,
- task_manager: server.plugins.task_manager,
+ taskManager: getTaskManagerSetup(server)!,
actions: server.plugins.actions.setup,
xpack_main: server.plugins.xpack_main,
encryptedSavedObjects: newPlatform.setup.plugins
@@ -137,7 +132,7 @@ export function shim(
spaces: () => server.plugins.spaces,
encryptedSavedObjects: newPlatform.start.plugins
.encryptedSavedObjects as EncryptedSavedObjectsStartContract,
- task_manager: server.plugins.task_manager,
+ taskManager: getTaskManagerStart(server)!,
};
return {
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts
index 87fa33a9cea58..394c13e1bd24f 100644
--- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts
@@ -7,7 +7,7 @@
import sinon from 'sinon';
import { schema } from '@kbn/config-schema';
import { AlertExecutorOptions } from '../types';
-import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager';
+import { ConcreteTaskInstance, TaskStatus } from '../../../../../plugins/task_manager/server';
import { TaskRunnerContext } from './task_runner_factory';
import { TaskRunner } from './task_runner';
import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
@@ -38,9 +38,7 @@ describe('Task Runner', () => {
scheduledAt: new Date(),
startedAt: new Date(),
retryAt: new Date(Date.now() + 5 * 60 * 1000),
- state: {
- startedAt: new Date(Date.now() - 5 * 60 * 1000),
- },
+ state: {},
taskType: 'alerting:test',
params: {
alertId: '1',
@@ -110,7 +108,13 @@ describe('Task Runner', () => {
test('successfully executes the task', async () => {
const taskRunner = new TaskRunner(
alertType,
- mockedTaskInstance,
+ {
+ ...mockedTaskInstance,
+ state: {
+ ...mockedTaskInstance.state,
+ previousStartedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
+ },
+ },
taskRunnerFactoryInitializerParams
);
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
@@ -141,6 +145,7 @@ describe('Task Runner', () => {
}
`);
expect(call.startedAt).toMatchInlineSnapshot(`1970-01-01T00:00:00.000Z`);
+ expect(call.previousStartedAt).toMatchInlineSnapshot(`1969-12-31T23:55:00.000Z`);
expect(call.state).toMatchInlineSnapshot(`Object {}`);
expect(call.name).toBe('alert-name');
expect(call.tags).toEqual(['alert-', '-tags']);
@@ -261,7 +266,6 @@ describe('Task Runner', () => {
"runAt": 1970-01-01T00:00:10.000Z,
"state": Object {
"previousStartedAt": 1970-01-01T00:00:00.000Z,
- "startedAt": 1969-12-31T23:55:00.000Z,
},
}
`);
@@ -293,7 +297,6 @@ describe('Task Runner', () => {
"runAt": 1970-01-01T00:00:10.000Z,
"state": Object {
"previousStartedAt": 1970-01-01T00:00:00.000Z,
- "startedAt": 1969-12-31T23:55:00.000Z,
},
}
`);
@@ -400,7 +403,96 @@ describe('Task Runner', () => {
"runAt": 1970-01-01T00:00:10.000Z,
"state": Object {
"previousStartedAt": 1970-01-01T00:00:00.000Z,
- "startedAt": 1969-12-31T23:55:00.000Z,
+ },
+ }
+ `);
+ });
+
+ test('recovers gracefully when the Alert Task Runner throws an exception when fetching the encrypted attributes', async () => {
+ encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockImplementation(() => {
+ throw new Error('OMG');
+ });
+
+ const taskRunner = new TaskRunner(
+ alertType,
+ mockedTaskInstance,
+ taskRunnerFactoryInitializerParams
+ );
+
+ savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+
+ const runnerResult = await taskRunner.run();
+
+ expect(runnerResult).toMatchInlineSnapshot(`
+ Object {
+ "runAt": 1970-01-01T00:05:00.000Z,
+ "state": Object {
+ "previousStartedAt": 1970-01-01T00:00:00.000Z,
+ },
+ }
+ `);
+ });
+
+ test('recovers gracefully when the Alert Task Runner throws an exception when getting internal Services', async () => {
+ taskRunnerFactoryInitializerParams.getServices.mockImplementation(() => {
+ throw new Error('OMG');
+ });
+
+ const taskRunner = new TaskRunner(
+ alertType,
+ mockedTaskInstance,
+ taskRunnerFactoryInitializerParams
+ );
+
+ savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+ encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ },
+ references: [],
+ });
+
+ const runnerResult = await taskRunner.run();
+
+ expect(runnerResult).toMatchInlineSnapshot(`
+ Object {
+ "runAt": 1970-01-01T00:05:00.000Z,
+ "state": Object {
+ "previousStartedAt": 1970-01-01T00:00:00.000Z,
+ },
+ }
+ `);
+ });
+
+ test('recovers gracefully when the Alert Task Runner throws an exception when fetching attributes', async () => {
+ savedObjectsClient.get.mockImplementation(() => {
+ throw new Error('OMG');
+ });
+
+ const taskRunner = new TaskRunner(
+ alertType,
+ mockedTaskInstance,
+ taskRunnerFactoryInitializerParams
+ );
+
+ encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+ id: '1',
+ type: 'alert',
+ attributes: {
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ },
+ references: [],
+ });
+
+ const runnerResult = await taskRunner.run();
+
+ expect(runnerResult).toMatchInlineSnapshot(`
+ Object {
+ "runAt": 1970-01-01T00:05:00.000Z,
+ "state": Object {
+ "previousStartedAt": 1970-01-01T00:00:00.000Z,
},
}
`);
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts
index 42c332e82e034..0f643e3d3121c 100644
--- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts
@@ -8,16 +8,23 @@ import { pick, mapValues, omit } from 'lodash';
import { Logger } from '../../../../../../src/core/server';
import { SavedObject } from '../../../../../../src/core/server';
import { TaskRunnerContext } from './task_runner_factory';
-import { ConcreteTaskInstance } from '../../../task_manager';
+import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server';
import { createExecutionHandler } from './create_execution_handler';
import { AlertInstance, createAlertInstanceFactory } from '../alert_instance';
import { getNextRunAt } from './get_next_run_at';
import { validateAlertTypeParams } from '../lib';
import { AlertType, RawAlert, IntervalSchedule, Services, State, AlertInfoParams } from '../types';
-import { promiseResult, map } from '../lib/result_type';
+import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
type AlertInstances = Record;
+const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' };
+
+interface AlertTaskRunResult {
+ state: State;
+ runAt: Date;
+}
+
export class TaskRunner {
private context: TaskRunnerContext;
private logger: Logger;
@@ -152,7 +159,7 @@ export class TaskRunner {
params,
state: alertTypeState,
startedAt: this.taskInstance.startedAt!,
- previousStartedAt,
+ previousStartedAt: previousStartedAt && new Date(previousStartedAt),
spaceId,
namespace,
name,
@@ -190,7 +197,7 @@ export class TaskRunner {
};
}
- async validateAndRunAlert(
+ async validateAndExecuteAlert(
services: Services,
apiKey: string | null,
attributes: RawAlert,
@@ -217,11 +224,9 @@ export class TaskRunner {
);
}
- async run() {
+ async loadAlertAttributesAndRun(): Promise> {
const {
params: { alertId, spaceId },
- startedAt: previousStartedAt,
- state: originalState,
} = this.taskInstance;
const apiKey = await this.getApiKeyForAlertPermissions(alertId, spaceId);
@@ -233,11 +238,34 @@ export class TaskRunner {
alertId
);
+ return {
+ state: await promiseResult(
+ this.validateAndExecuteAlert(services, apiKey, attributes, references)
+ ),
+ runAt: asOk(
+ getNextRunAt(
+ new Date(this.taskInstance.startedAt!),
+ // we do not currently have a good way of returning the type
+ // from SavedObjectsClient, and as we currenrtly require a schedule
+ // and we only support `interval`, we can cast this safely
+ attributes.schedule as IntervalSchedule
+ )
+ ),
+ };
+ }
+
+ async run(): Promise {
+ const {
+ params: { alertId },
+ startedAt: previousStartedAt,
+ state: originalState,
+ } = this.taskInstance;
+
+ const { state, runAt } = await errorAsAlertTaskRunResult(this.loadAlertAttributesAndRun());
+
return {
state: map(
- await promiseResult(
- this.validateAndRunAlert(services, apiKey, attributes, references)
- ),
+ state,
(stateUpdates: State) => {
return {
...stateUpdates,
@@ -252,13 +280,32 @@ export class TaskRunner {
};
}
),
- runAt: getNextRunAt(
- new Date(this.taskInstance.startedAt!),
- // we do not currently have a good way of returning the type
- // from SavedObjectsClient, and as we currenrtly require a schedule
- // and we only support `interval`, we can cast this safely
- attributes.schedule as IntervalSchedule
+ runAt: resolveErr(runAt, () =>
+ getNextRunAt(
+ new Date(),
+ // if we fail at this point we wish to recover but don't have access to the Alert's
+ // attributes, so we'll use a default interval to prevent the underlying task from
+ // falling into a failed state
+ FALLBACK_RETRY_INTERVAL
+ )
),
};
}
}
+
+/**
+ * If an error is thrown, wrap it in an AlertTaskRunResult
+ * so that we can treat each field independantly
+ */
+async function errorAsAlertTaskRunResult(
+ future: Promise>
+): Promise> {
+ try {
+ return await future;
+ } catch (e) {
+ return {
+ state: asErr(e),
+ runAt: asErr(e),
+ };
+ }
+}
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts
index 2ea1256352bec..543b9e7d32e12 100644
--- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts
@@ -5,7 +5,7 @@
*/
import sinon from 'sinon';
-import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager';
+import { ConcreteTaskInstance, TaskStatus } from '../../../../../plugins/task_manager/server';
import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory';
import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
import {
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts
index 7186e1e729bda..7178fa4f01282 100644
--- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger } from '../../../../../../src/core/server';
-import { RunContext } from '../../../task_manager';
+import { RunContext } from '../../../../../plugins/task_manager/server';
import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server';
import { PluginStartContract as ActionsPluginStartContract } from '../../../actions';
import {
diff --git a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
index e345ca3552e5a..8f87b3473b2e4 100644
--- a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
@@ -4,6 +4,8 @@ exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Error CONTAINER_ID 1`] = `undefined`;
+exports[`Error DESTINATION_ADDRESS 1`] = `undefined`;
+
exports[`Error ERROR_CULPRIT 1`] = `"handleOopsie"`;
exports[`Error ERROR_EXC_HANDLED 1`] = `undefined`;
@@ -112,6 +114,8 @@ exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Span CONTAINER_ID 1`] = `undefined`;
+exports[`Span DESTINATION_ADDRESS 1`] = `undefined`;
+
exports[`Span ERROR_CULPRIT 1`] = `undefined`;
exports[`Span ERROR_EXC_HANDLED 1`] = `undefined`;
@@ -220,6 +224,8 @@ exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
+exports[`Transaction DESTINATION_ADDRESS 1`] = `undefined`;
+
exports[`Transaction ERROR_CULPRIT 1`] = `undefined`;
exports[`Transaction ERROR_EXC_HANDLED 1`] = `undefined`;
diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts
index 0d7ff3114e73f..ce2db4964a412 100644
--- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts
+++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts
@@ -14,6 +14,8 @@ export const HTTP_REQUEST_METHOD = 'http.request.method';
export const USER_ID = 'user.id';
export const USER_AGENT_NAME = 'user_agent.name';
+export const DESTINATION_ADDRESS = 'destination.address';
+
export const OBSERVER_VERSION_MAJOR = 'observer.version_major';
export const OBSERVER_LISTENING = 'observer.listening';
export const PROCESSOR_EVENT = 'processor.event';
diff --git a/x-pack/legacy/plugins/apm/common/service_map.ts b/x-pack/legacy/plugins/apm/common/service_map.ts
new file mode 100644
index 0000000000000..fbaa489c45039
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/common/service_map.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface ServiceConnectionNode {
+ 'service.name': string;
+ 'service.environment': string | null;
+ 'agent.name': string;
+}
+export interface ExternalConnectionNode {
+ 'destination.address': string;
+ 'span.type': string;
+ 'span.subtype': string;
+}
+
+export type ConnectionNode = ServiceConnectionNode | ExternalConnectionNode;
+
+export interface Connection {
+ source: ConnectionNode;
+ destination: ConnectionNode;
+}
diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts
index cf2cbd2507215..0934cb0019f44 100644
--- a/x-pack/legacy/plugins/apm/index.ts
+++ b/x-pack/legacy/plugins/apm/index.ts
@@ -71,7 +71,8 @@ export const apm: LegacyPluginInitializer = kibana => {
autocreateApmIndexPattern: Joi.boolean().default(true),
// service map
- serviceMapEnabled: Joi.boolean().default(false)
+ serviceMapEnabled: Joi.boolean().default(false),
+ serviceMapInitialTimeRange: Joi.number().default(60 * 1000 * 60) // last 1 hour
}).default();
},
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 238158c5bf224..d69fa5d895b9e 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,18 +69,41 @@ 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');
cy.getElementById(serviceName).addClass('primary');
}
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/LoadingOverlay.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx
new file mode 100644
index 0000000000000..efafdbcecd41c
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import theme from '@elastic/eui/dist/eui_theme_light.json';
+import React from 'react';
+import { EuiProgress, EuiText, EuiSpacer } from '@elastic/eui';
+import styled from 'styled-components';
+import { i18n } from '@kbn/i18n';
+
+const Container = styled.div`
+ position: relative;
+`;
+
+const Overlay = styled.div`
+ position: absolute;
+ top: 0;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ padding: ${theme.gutterTypes.gutterMedium};
+`;
+
+const ProgressBarContainer = styled.div`
+ width: 50%;
+ max-width: 600px;
+`;
+
+interface Props {
+ children: React.ReactNode;
+ isLoading: boolean;
+ percentageLoaded: number;
+}
+
+export const LoadingOverlay = ({
+ children,
+ isLoading,
+ percentageLoaded
+}: Props) => (
+
+ {isLoading && (
+
+
+
+
+
+
+ {i18n.translate('xpack.apm.loadingServiceMap', {
+ defaultMessage:
+ 'Loading service map... This might take a short while.'
+ })}
+
+
+ )}
+ {children}
+
+);
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 0000000000000..a8c45c83a382a
--- /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 0000000000000..1c5443e404f9b
--- /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 0000000000000..8ce6d9d57c4ac
--- /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 0000000000000..dfb78aaa0214c
--- /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 03ae9d0c287e5..1a6247388a655 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,22 +3,18 @@
* 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 = {
- animate: true,
- animationEasing: theme.euiAnimSlightBounce as cytoscape.Css.TransitionTimingFunction,
- animationDuration: parseInt(theme.euiAnimSpeedFast, 10),
name: 'dagre',
nodeDimensionsIncludeLabels: true,
- rankDir: 'LR',
- spacingFactor: 2
+ rankDir: 'LR'
};
-function isDatabaseOrExternal(agentName: string) {
- return agentName === 'database' || agentName === 'external';
+function isService(el: cytoscape.NodeSingular) {
+ return el.data('type') === 'service';
}
const style: cytoscape.Stylesheet[] = [
@@ -31,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,11 +43,11 @@ const style: cytoscape.Stylesheet[] = [
'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif',
'font-size': theme.euiFontSizeXS,
height: theme.avatarSizing.l.size,
- label: 'data(id)',
+ label: 'data(label)',
'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,
@@ -76,14 +72,24 @@ const style: cytoscape.Stylesheet[] = [
//
// @ts-ignore
'target-distance-from-node': theme.paddingSizes.xs,
- width: 2
+ width: 1,
+ 'source-arrow-shape': 'none'
+ }
+ },
+ {
+ selector: 'edge[bidirectional]',
+ style: {
+ 'source-arrow-shape': 'triangle',
+ 'target-arrow-shape': 'triangle',
+ // @ts-ignore
+ 'source-distance-from-node': theme.paddingSizes.xs,
+ 'target-distance-from-node': theme.paddingSizes.xs
}
}
];
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
new file mode 100644
index 0000000000000..106e9a1d82f29
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts
@@ -0,0 +1,168 @@
+/*
+ * 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 { ValuesType } from 'utility-types';
+import { sortBy, isEqual } from 'lodash';
+import { Connection, ConnectionNode } from '../../../../common/service_map';
+import { ServiceMapAPIResponse } from '../../../../server/lib/service_map/get_service_map';
+import { getAPMHref } from '../../shared/Links/apm/APMLink';
+
+function getConnectionNodeId(node: ConnectionNode): string {
+ if ('destination.address' in node) {
+ // use a prefix to distinguish exernal destination ids from services
+ return `>${node['destination.address']}`;
+ }
+ return node['service.name'];
+}
+
+function getConnectionId(connection: Connection) {
+ return `${getConnectionNodeId(connection.source)}~${getConnectionNodeId(
+ connection.destination
+ )}`;
+}
+export function getCytoscapeElements(
+ responses: ServiceMapAPIResponse[],
+ search: string
+) {
+ const discoveredServices = responses.flatMap(
+ response => response.discoveredServices
+ );
+
+ const serviceNodes = responses
+ .flatMap(response => response.services)
+ .map(service => ({
+ ...service,
+ id: service['service.name']
+ }));
+
+ // maps destination.address to service.name if possible
+ function getConnectionNode(node: ConnectionNode) {
+ let mappedNode: ConnectionNode | undefined;
+
+ if ('destination.address' in node) {
+ mappedNode = discoveredServices.find(map => isEqual(map.from, node))?.to;
+ }
+
+ if (!mappedNode) {
+ mappedNode = node;
+ }
+
+ return {
+ ...mappedNode,
+ id: getConnectionNodeId(mappedNode)
+ };
+ }
+
+ // build connections with mapped nodes
+ const connections = responses
+ .flatMap(response => response.connections)
+ .map(connection => {
+ const source = getConnectionNode(connection.source);
+ const destination = getConnectionNode(connection.destination);
+
+ return {
+ source,
+ destination,
+ id: getConnectionId({ source, destination })
+ };
+ })
+ .filter(connection => connection.source.id !== connection.destination.id);
+
+ const nodes = connections
+ .flatMap(connection => [connection.source, connection.destination])
+ .concat(serviceNodes);
+
+ type ConnectionWithId = ValuesType;
+ type ConnectionNodeWithId = ValuesType;
+
+ const connectionsById = connections.reduce((connectionMap, connection) => {
+ return {
+ ...connectionMap,
+ [connection.id]: connection
+ };
+ }, {} as Record);
+
+ const nodesById = nodes.reduce((nodeMap, node) => {
+ return {
+ ...nodeMap,
+ [node.id]: node
+ };
+ }, {} as Record);
+
+ const cyNodes = (Object.values(nodesById) as ConnectionNodeWithId[]).map(
+ node => {
+ let data = {};
+
+ if ('service.name' in node) {
+ data = {
+ href: getAPMHref(
+ `/services/${node['service.name']}/service-map`,
+ search
+ ),
+ 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']
+ };
+ }
+
+ return {
+ group: 'nodes' as const,
+ data: {
+ id: node.id,
+ label:
+ 'service.name' in node
+ ? node['service.name']
+ : node['destination.address'],
+ ...data
+ }
+ };
+ }
+ );
+
+ // instead of adding connections in two directions,
+ // we add a `bidirectional` flag to use in styling
+ const dedupedConnections = (sortBy(
+ Object.values(connectionsById),
+ // make sure that order is stable
+ 'id'
+ ) as ConnectionWithId[]).reduce<
+ Array
+ >((prev, connection) => {
+ const reversedConnection = prev.find(
+ c =>
+ c.destination.id === connection.source.id &&
+ c.source.id === connection.destination.id
+ );
+
+ if (reversedConnection) {
+ reversedConnection.bidirectional = true;
+ return prev;
+ }
+
+ return prev.concat(connection);
+ }, []);
+
+ const cyEdges = dedupedConnections.map(connection => {
+ return {
+ group: 'edges' as const,
+ data: {
+ id: connection.id,
+ source: connection.source.id,
+ target: connection.destination.id,
+ bidirectional: connection.bidirectional ? true : undefined
+ }
+ };
+ }, []);
+
+ return [...cyNodes, ...cyEdges];
+}
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 d5cfb49e458c6..722f64c6a7e58 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 0000000000000..b0648d14f20ba
--- /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 cc09975a344b5..a8e6f964f4d0c 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,14 +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 React from 'react';
-import { useFetcher } from '../../../hooks/useFetcher';
+import { i18n } from '@kbn/i18n';
+import { ElementDefinition } from 'cytoscape';
+import { find, isEqual } from 'lodash';
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState
+} from 'react';
+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 { getCytoscapeElements } from './get_cytoscape_elements';
+import { LoadingOverlay } from './LoadingOverlay';
import { PlatinumLicensePrompt } from './PlatinumLicensePrompt';
+import { Popover } from './Popover';
interface ServiceMapProps {
serviceName?: string;
@@ -37,37 +55,160 @@ ${theme.euiColorLightShade}`,
margin: `-${theme.gutterTypes.gutterLarge}`
};
+const MAX_REQUESTS = 5;
+
export function ServiceMap({ serviceName }: ServiceMapProps) {
- const {
- urlParams: { start, end }
- } = useUrlParams();
+ const callApmApi = useCallApmApi();
+ const license = useLicense();
+ const { search } = useLocation();
+ const { urlParams, uiFilters } = useUrlParams();
+ const { notifications } = useApmPluginContext().core;
+ const params = useDeepObjectIdentity({
+ start: urlParams.start,
+ end: urlParams.end,
+ environment: urlParams.environment,
+ serviceName,
+ uiFilters: {
+ ...uiFilters,
+ environment: undefined
+ }
+ });
+
+ const renderedElements = useRef([]);
+ const openToast = useRef(null);
+
+ const [responses, setResponses] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [percentageLoaded, setPercentageLoaded] = useState(0);
+ const [, _setUnusedState] = useState(false);
+
+ const elements = useMemo(() => getCytoscapeElements(responses, search), [
+ responses,
+ search
+ ]);
+
+ const forceUpdate = useCallback(() => _setUnusedState(value => !value), []);
+
+ const getNext = useCallback(
+ async (input: { reset?: boolean; after?: string | undefined }) => {
+ const { start, end, uiFilters: strippedUiFilters, ...query } = params;
+
+ if (input.reset) {
+ renderedElements.current = [];
+ setResponses([]);
+ }
- const { data } = useFetcher(
- callApmApi => {
if (start && end) {
- return callApmApi({
- pathname: '/api/apm/service-map',
- params: { query: { start, end } }
- });
+ setIsLoading(true);
+ try {
+ const data = await callApmApi({
+ pathname: '/api/apm/service-map',
+ params: {
+ query: {
+ ...query,
+ start,
+ end,
+ uiFilters: JSON.stringify(strippedUiFilters),
+ after: input.after
+ }
+ }
+ });
+ setResponses(resp => resp.concat(data));
+ setIsLoading(false);
+
+ const shouldGetNext =
+ responses.length + 1 < MAX_REQUESTS && data.after;
+
+ if (shouldGetNext) {
+ setPercentageLoaded(value => value + 30); // increase loading bar 30%
+ await getNext({ after: data.after });
+ }
+ } catch (error) {
+ setIsLoading(false);
+ notifications.toasts.addError(error, {
+ title: i18n.translate('xpack.apm.errorServiceMapData', {
+ defaultMessage: `Error loading service connections`
+ })
+ });
+ }
}
},
- [start, end]
+ [callApmApi, params, responses.length, notifications.toasts]
);
- const elements = Array.isArray(data) ? data : [];
- const license = useLicense();
+ useEffect(() => {
+ const loadServiceMaps = async () => {
+ setPercentageLoaded(5);
+ await getNext({ reset: true });
+ setPercentageLoaded(100);
+ };
+
+ loadServiceMaps();
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [params]);
+
+ useEffect(() => {
+ if (renderedElements.current.length === 0) {
+ renderedElements.current = elements;
+ return;
+ }
+
+ const newElements = elements.filter(element => {
+ return !find(renderedElements.current, el => isEqual(el, element));
+ });
+
+ const updateMap = () => {
+ renderedElements.current = elements;
+ if (openToast.current) {
+ notifications.toasts.remove(openToast.current);
+ }
+ forceUpdate();
+ };
+
+ if (newElements.length > 0 && percentageLoaded === 100) {
+ openToast.current = notifications.toasts.add({
+ title: i18n.translate('xpack.apm.newServiceMapData', {
+ defaultMessage: `Newly discovered connections are available.`
+ }),
+ onClose: () => {
+ openToast.current = null;
+ },
+ toastLifeTimeMs: 24 * 60 * 60 * 1000,
+ text: toMountPoint(
+
+ {i18n.translate('xpack.apm.updateServiceMap', {
+ defaultMessage: 'Update map'
+ })}
+
+ )
+ }).id;
+ }
+
+ return () => {
+ if (openToast.current) {
+ notifications.toasts.remove(openToast.current);
+ }
+ };
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [elements, percentageLoaded]);
+
const isValidPlatinumLicense =
license?.isActive &&
(license?.type === 'platinum' || license?.type === 'trial');
return isValidPlatinumLicense ? (
-
-
-
+
+
+
+
+
+
) : (
);
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 ece396bc4cfc4..c95855c117047 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": "#5bbaa0",
},
}
`;
@@ -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 59679bfe11641..655fc5a25b9ef 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 25f8128b27211..a8e6bc0a648af 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 84c2801a45049..51056fae50360 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 557751a0f0226..1bf125c301644 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": "#fae181",
"disabled": undefined,
"onClick": [Function],
"text":
@@ -22,7 +22,7 @@ Array [
,
},
Object {
- "color": "#f98510",
+ "color": "#f19f58",
"disabled": undefined,
"onClick": [Function],
"text":
@@ -442,7 +442,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -463,9 +463,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -480,9 +480,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -497,9 +497,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -514,9 +514,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -531,9 +531,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -548,9 +548,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -565,9 +565,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -582,9 +582,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -599,9 +599,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -616,9 +616,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -633,9 +633,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -650,9 +650,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -667,9 +667,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -684,9 +684,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -701,9 +701,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -718,9 +718,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -735,9 +735,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -752,9 +752,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -769,9 +769,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -786,9 +786,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -803,9 +803,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -820,9 +820,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -837,9 +837,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -854,9 +854,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -871,9 +871,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -888,9 +888,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -905,9 +905,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -922,9 +922,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -939,9 +939,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -956,9 +956,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -973,9 +973,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -995,7 +995,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -1016,9 +1016,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1033,9 +1033,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1050,9 +1050,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1067,9 +1067,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1084,9 +1084,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1101,9 +1101,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1118,9 +1118,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1135,9 +1135,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1152,9 +1152,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1169,9 +1169,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1186,9 +1186,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1203,9 +1203,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1220,9 +1220,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1237,9 +1237,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1254,9 +1254,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1271,9 +1271,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1288,9 +1288,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1305,9 +1305,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1322,9 +1322,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1339,9 +1339,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1356,9 +1356,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1373,9 +1373,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1390,9 +1390,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1407,9 +1407,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1424,9 +1424,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1441,9 +1441,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1458,9 +1458,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1475,9 +1475,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1492,9 +1492,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1509,9 +1509,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -1526,9 +1526,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"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: #fae181;
border-radius: 100%;
}
@@ -2667,7 +2667,7 @@ Array [
width: 11px;
height: 11px;
margin-right: 5.5px;
- background: #f98510;
+ background: #f19f58;
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": "#fae181",
"text": "95th",
"value": 1557383.999999999,
},
Object {
- "color": "#f98510",
+ "color": "#f19f58",
"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: #fae181;
border-radius: 100%;
}
@@ -2907,7 +2907,7 @@ Array [
width: 8px;
height: 8px;
margin-right: 4px;
- background: #f98510;
+ background: #f19f58;
border-radius: 100%;
}
@@ -3378,7 +3378,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -3399,9 +3399,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3416,9 +3416,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3433,9 +3433,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3450,9 +3450,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3467,9 +3467,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3484,9 +3484,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3501,9 +3501,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3518,9 +3518,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3535,9 +3535,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3552,9 +3552,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3569,9 +3569,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3586,9 +3586,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3603,9 +3603,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3620,9 +3620,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3637,9 +3637,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3654,9 +3654,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3671,9 +3671,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3688,9 +3688,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3705,9 +3705,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3722,9 +3722,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3739,9 +3739,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3756,9 +3756,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3773,9 +3773,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3790,9 +3790,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3807,9 +3807,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3824,9 +3824,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3841,9 +3841,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3858,9 +3858,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3875,9 +3875,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3892,9 +3892,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3909,9 +3909,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#f98510",
+ "fill": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -3931,7 +3931,7 @@ Array [
style={
Object {
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeDasharray": undefined,
"strokeWidth": undefined,
}
@@ -3952,9 +3952,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -3969,9 +3969,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -3986,9 +3986,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4003,9 +4003,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4020,9 +4020,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4037,9 +4037,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4054,9 +4054,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4071,9 +4071,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4088,9 +4088,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4105,9 +4105,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4122,9 +4122,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4139,9 +4139,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4156,9 +4156,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4173,9 +4173,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4190,9 +4190,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4207,9 +4207,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4224,9 +4224,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4241,9 +4241,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4258,9 +4258,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4275,9 +4275,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4292,9 +4292,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4309,9 +4309,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4326,9 +4326,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4343,9 +4343,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4360,9 +4360,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4377,9 +4377,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4394,9 +4394,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4411,9 +4411,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4428,9 +4428,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4445,9 +4445,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"strokeWidth": 1,
}
}
@@ -4462,9 +4462,9 @@ Array [
r={0.5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"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": "#f19f58",
"opacity": 1,
- "stroke": "#f98510",
+ "stroke": "#f19f58",
"strokeWidth": 1,
}
}
@@ -5089,9 +5089,9 @@ Array [
r={5}
style={
Object {
- "fill": "#e6c220",
+ "fill": "#fae181",
"opacity": 1,
- "stroke": "#e6c220",
+ "stroke": "#fae181",
"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: #fae181;
border-radius: 100%;
}
@@ -5846,7 +5846,7 @@ Array [
width: 11px;
height: 11px;
margin-right: 5.5px;
- background: #f98510;
+ background: #f19f58;
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 da71e264ac099..f1c7d4826fe0c 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 0bd3896782603..62cdbd3bbc995 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/selectors/__tests__/chartSelectors.test.ts b/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
index 2b0263f69db8f..1218bc726c3b7 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: '#fae181',
data: [
{ x: 0, y: 200 },
{ x: 1000, y: 300 }
@@ -77,7 +77,7 @@ describe('chartSelectors', () => {
type: 'linemark'
},
{
- color: '#f98510',
+ color: '#f19f58',
data: [
{ x: 0, y: 300 },
{ x: 1000, y: 400 }
diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts
index aeeb39733b5db..737eeac95516e 100644
--- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts
@@ -11,7 +11,7 @@ import {
IndicesDeleteParams,
IndicesCreateParams
} from 'elasticsearch';
-import { merge } from 'lodash';
+import { merge, uniqueId } from 'lodash';
import { cloneDeep, isString } from 'lodash';
import { KibanaRequest } from 'src/core/server';
import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames';
@@ -127,6 +127,23 @@ export function getESClient(
? callAsInternalUser
: callAsCurrentUser;
+ const debug = context.params.query._debug;
+
+ function withTime(
+ fn: (log: typeof console.log) => Promise
+ ): Promise {
+ const log = console.log.bind(console, uniqueId());
+ if (!debug) {
+ return fn(log);
+ }
+ const time = process.hrtime();
+ return fn(log).then(data => {
+ const now = process.hrtime(time);
+ log(`took: ${Math.round(now[0] * 1000 + now[1] / 1e6)}ms`);
+ return data;
+ });
+ }
+
return {
search: async <
TDocument = unknown,
@@ -141,27 +158,29 @@ export function getESClient(
apmOptions
);
- if (context.params.query._debug) {
- console.log(`--DEBUG ES QUERY--`);
- console.log(
- `${request.url.pathname} ${JSON.stringify(context.params.query)}`
- );
- console.log(`GET ${nextParams.index}/_search`);
- console.log(JSON.stringify(nextParams.body, null, 2));
- }
+ return withTime(log => {
+ if (context.params.query._debug) {
+ log(`--DEBUG ES QUERY--`);
+ log(
+ `${request.url.pathname} ${JSON.stringify(context.params.query)}`
+ );
+ log(`GET ${nextParams.index}/_search`);
+ log(JSON.stringify(nextParams.body, null, 2));
+ }
- return (callMethod('search', nextParams) as unknown) as Promise<
- ESSearchResponse
- >;
+ return (callMethod('search', nextParams) as unknown) as Promise<
+ ESSearchResponse
+ >;
+ });
},
index: (params: APMIndexDocumentParams) => {
- return callMethod('index', params);
+ return withTime(() => callMethod('index', params));
},
delete: (params: IndicesDeleteParams) => {
- return callMethod('delete', params);
+ return withTime(() => callMethod('delete', params));
},
indicesCreate: (params: IndicesCreateParams) => {
- return callMethod('indices.create', params);
+ return withTime(() => callMethod('indices.create', params));
}
};
}
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 8c6ed2ebcec75..870660c429ca3 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.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map.ts
new file mode 100644
index 0000000000000..04e2a43a4b8f1
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map.ts
@@ -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 { PromiseReturnType } from '../../../typings/common';
+import {
+ Setup,
+ SetupTimeRange,
+ SetupUIFilters
+} from '../helpers/setup_request';
+import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids';
+import { getTraceSampleIds } from './get_trace_sample_ids';
+import { getServicesProjection } from '../../../common/projections/services';
+import { mergeProjection } from '../../../common/projections/util/merge_projection';
+import {
+ SERVICE_AGENT_NAME,
+ SERVICE_NAME
+} from '../../../common/elasticsearch_fieldnames';
+
+export interface IEnvOptions {
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+ serviceName?: string;
+ environment?: string;
+ after?: string;
+}
+
+async function getConnectionData({
+ setup,
+ serviceName,
+ environment,
+ after
+}: IEnvOptions) {
+ const { traceIds, after: nextAfter } = await getTraceSampleIds({
+ setup,
+ serviceName,
+ environment,
+ after
+ });
+
+ const serviceMapData = traceIds.length
+ ? await getServiceMapFromTraceIds({
+ setup,
+ serviceName,
+ environment,
+ traceIds
+ })
+ : { connections: [], discoveredServices: [] };
+
+ return {
+ after: nextAfter,
+ ...serviceMapData
+ };
+}
+
+async function getServicesData(options: IEnvOptions) {
+ // only return services on the first request for the global service map
+ if (options.after) {
+ return [];
+ }
+
+ const { setup } = options;
+
+ const projection = getServicesProjection({ setup });
+
+ const { filter } = projection.body.query.bool;
+
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ ...projection.body.query.bool,
+ filter: options.serviceName
+ ? filter.concat({
+ term: {
+ [SERVICE_NAME]: options.serviceName
+ }
+ })
+ : filter
+ }
+ },
+ aggs: {
+ services: {
+ terms: {
+ field: projection.body.aggs.services.terms.field,
+ size: 500
+ },
+ aggs: {
+ agent_name: {
+ terms: {
+ field: SERVICE_AGENT_NAME
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+
+ const { client } = setup;
+
+ const response = await client.search(params);
+
+ return (
+ response.aggregations?.services.buckets.map(bucket => {
+ return {
+ 'service.name': bucket.key as string,
+ 'agent.name':
+ (bucket.agent_name.buckets[0]?.key as string | undefined) || '',
+ 'service.environment': options.environment || null
+ };
+ }) || []
+ );
+}
+
+export type ServiceMapAPIResponse = PromiseReturnType;
+export async function getServiceMap(options: IEnvOptions) {
+ const [connectionData, servicesData] = await Promise.all([
+ getConnectionData(options),
+ getServicesData(options)
+ ]);
+
+ return {
+ ...connectionData,
+ services: servicesData
+ };
+}
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
new file mode 100644
index 0000000000000..d3711e9582d15
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts
@@ -0,0 +1,281 @@
+/*
+ * 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 { uniq, find } from 'lodash';
+import { Setup } from '../helpers/setup_request';
+import {
+ TRACE_ID,
+ PROCESSOR_EVENT
+} from '../../../common/elasticsearch_fieldnames';
+import {
+ Connection,
+ ServiceConnectionNode,
+ ConnectionNode,
+ ExternalConnectionNode
+} from '../../../common/service_map';
+
+export async function getServiceMapFromTraceIds({
+ setup,
+ traceIds,
+ serviceName,
+ environment
+}: {
+ setup: Setup;
+ traceIds: string[];
+ serviceName?: string;
+ environment?: string;
+}) {
+ const { indices, client } = setup;
+
+ const serviceMapParams = {
+ index: [
+ indices['apm_oss.spanIndices'],
+ indices['apm_oss.transactionIndices']
+ ],
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ {
+ terms: {
+ [PROCESSOR_EVENT]: ['span', 'transaction']
+ }
+ },
+ {
+ terms: {
+ [TRACE_ID]: traceIds
+ }
+ }
+ ]
+ }
+ },
+ aggs: {
+ service_map: {
+ scripted_metric: {
+ init_script: {
+ lang: 'painless',
+ source: `state.eventsById = new HashMap();
+
+ String[] fieldsToCopy = new String[] {
+ 'parent.id',
+ 'service.name',
+ 'service.environment',
+ 'destination.address',
+ 'trace.id',
+ 'processor.event',
+ 'span.type',
+ 'span.subtype',
+ 'agent.name'
+ };
+ state.fieldsToCopy = fieldsToCopy;`
+ },
+ map_script: {
+ lang: 'painless',
+ source: `def id;
+ if (!doc['span.id'].empty) {
+ id = doc['span.id'].value;
+ } else {
+ id = doc['transaction.id'].value;
+ }
+
+ def copy = new HashMap();
+ copy.id = id;
+
+ for(key in state.fieldsToCopy) {
+ if (!doc[key].empty) {
+ copy[key] = doc[key].value;
+ }
+ }
+
+ state.eventsById[id] = copy`
+ },
+ combine_script: {
+ lang: 'painless',
+ source: `return state.eventsById;`
+ },
+ reduce_script: {
+ lang: 'painless',
+ source: `
+ def getDestination ( def event ) {
+ def destination = new HashMap();
+ destination['destination.address'] = event['destination.address'];
+ destination['span.type'] = event['span.type'];
+ destination['span.subtype'] = event['span.subtype'];
+ return destination;
+ }
+
+ def processAndReturnEvent(def context, def eventId) {
+ if (context.processedEvents[eventId] != null) {
+ return context.processedEvents[eventId];
+ }
+
+ def event = context.eventsById[eventId];
+
+ if (event == null) {
+ return null;
+ }
+
+ def service = new HashMap();
+ service['service.name'] = event['service.name'];
+ service['service.environment'] = event['service.environment'];
+ service['agent.name'] = event['agent.name'];
+
+ def basePath = new ArrayList();
+
+ def parentId = event['parent.id'];
+ def parent;
+
+ if (parentId != null && parentId != event['id']) {
+ parent = processAndReturnEvent(context, parentId);
+ if (parent != null) {
+ /* copy the path from the parent */
+ basePath.addAll(parent.path);
+ /* flag parent path for removal, as it has children */
+ context.locationsToRemove.add(parent.path);
+
+ /* if the parent has 'destination.address' set, and the service is different,
+ we've discovered a service */
+
+ if (parent['destination.address'] != null
+ && parent['destination.address'] != ""
+ && (parent['span.type'] == 'external'
+ || parent['span.type'] == 'messaging')
+ && (parent['service.name'] != event['service.name']
+ || parent['service.environment'] != event['service.environment']
+ )
+ ) {
+ def parentDestination = getDestination(parent);
+ context.externalToServiceMap.put(parentDestination, service);
+ }
+ }
+ }
+
+ def lastLocation = basePath.size() > 0 ? basePath[basePath.size() - 1] : null;
+
+ def currentLocation = service;
+
+ /* only add the current location to the path if it's different from the last one*/
+ if (lastLocation == null || !lastLocation.equals(currentLocation)) {
+ basePath.add(currentLocation);
+ }
+
+ /* if there is an outgoing span, create a new path */
+ if (event['destination.address'] != null
+ && event['destination.address'] != '') {
+ def outgoingLocation = getDestination(event);
+ def outgoingPath = new ArrayList(basePath);
+ outgoingPath.add(outgoingLocation);
+ context.paths.add(outgoingPath);
+ }
+
+ event.path = basePath;
+
+ context.processedEvents[eventId] = event;
+ return event;
+ }
+
+ def context = new HashMap();
+
+ context.processedEvents = new HashMap();
+ context.eventsById = new HashMap();
+
+ context.paths = new HashSet();
+ context.externalToServiceMap = new HashMap();
+ context.locationsToRemove = new HashSet();
+
+ for (state in states) {
+ context.eventsById.putAll(state);
+ }
+
+ for (entry in context.eventsById.entrySet()) {
+ processAndReturnEvent(context, entry.getKey());
+ }
+
+ def paths = new HashSet();
+
+ for(foundPath in context.paths) {
+ if (!context.locationsToRemove.contains(foundPath)) {
+ paths.add(foundPath);
+ }
+ }
+
+ def response = new HashMap();
+ response.paths = paths;
+
+ def discoveredServices = new HashSet();
+
+ for(entry in context.externalToServiceMap.entrySet()) {
+ def map = new HashMap();
+ map.from = entry.getKey();
+ map.to = entry.getValue();
+ discoveredServices.add(map);
+ }
+ response.discoveredServices = discoveredServices;
+
+ return response;`
+ }
+ }
+ }
+ }
+ }
+ };
+
+ const serviceMapResponse = await client.search(serviceMapParams);
+
+ const scriptResponse = serviceMapResponse.aggregations?.service_map.value as {
+ paths: ConnectionNode[][];
+ discoveredServices: Array<{
+ from: ExternalConnectionNode;
+ to: ServiceConnectionNode;
+ }>;
+ };
+
+ let paths = scriptResponse.paths;
+
+ if (serviceName || environment) {
+ paths = paths.filter(path => {
+ return path.some(node => {
+ let matches = true;
+ if (serviceName) {
+ matches =
+ matches &&
+ 'service.name' in node &&
+ node['service.name'] === serviceName;
+ }
+ if (environment) {
+ matches =
+ matches &&
+ 'service.environment' in node &&
+ node['service.environment'] === environment;
+ }
+ return matches;
+ });
+ });
+ }
+
+ const connections = uniq(
+ paths.flatMap(path => {
+ return path.reduce((conns, location, index) => {
+ const prev = path[index - 1];
+ if (prev) {
+ return conns.concat({
+ source: prev,
+ destination: location
+ });
+ }
+ return conns;
+ }, [] as Connection[]);
+ }, [] as Connection[]),
+ (value, index, array) => {
+ return find(array, value);
+ }
+ );
+
+ return {
+ connections,
+ discoveredServices: scriptResponse.discoveredServices
+ };
+}
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 0000000000000..6c4d540103cec
--- /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/service_map/get_trace_sample_ids.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts
new file mode 100644
index 0000000000000..acf113b426608
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts
@@ -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 { uniq, take, sortBy } from 'lodash';
+import {
+ Setup,
+ SetupUIFilters,
+ SetupTimeRange
+} from '../helpers/setup_request';
+import { rangeFilter } from '../helpers/range_filter';
+import { ESFilter } from '../../../typings/elasticsearch';
+import {
+ PROCESSOR_EVENT,
+ SERVICE_NAME,
+ SERVICE_ENVIRONMENT,
+ SPAN_TYPE,
+ SPAN_SUBTYPE,
+ DESTINATION_ADDRESS,
+ TRACE_ID
+} from '../../../common/elasticsearch_fieldnames';
+
+const MAX_TRACES_TO_INSPECT = 1000;
+
+export async function getTraceSampleIds({
+ after,
+ serviceName,
+ environment,
+ setup
+}: {
+ after?: string;
+ serviceName?: string;
+ environment?: string;
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+}) {
+ const isTop = !after;
+
+ const { start, end, client, indices, config } = setup;
+
+ const rangeEnd = end;
+ const rangeStart = isTop
+ ? rangeEnd - config['xpack.apm.serviceMapInitialTimeRange']
+ : start;
+
+ const rangeQuery = { range: rangeFilter(rangeStart, rangeEnd) };
+
+ const query = {
+ bool: {
+ filter: [
+ {
+ term: {
+ [PROCESSOR_EVENT]: 'span'
+ }
+ },
+ {
+ exists: {
+ field: DESTINATION_ADDRESS
+ }
+ },
+ rangeQuery
+ ] as ESFilter[]
+ }
+ } as { bool: { filter: ESFilter[]; must_not?: ESFilter[] | ESFilter } };
+
+ if (serviceName) {
+ query.bool.filter.push({ term: { [SERVICE_NAME]: serviceName } });
+ }
+
+ if (environment) {
+ query.bool.filter.push({ term: { [SERVICE_ENVIRONMENT]: environment } });
+ }
+
+ const afterObj =
+ after && after !== 'top'
+ ? { after: JSON.parse(Buffer.from(after, 'base64').toString()) }
+ : {};
+
+ const params = {
+ index: [indices['apm_oss.spanIndices']],
+ body: {
+ size: 0,
+ query,
+ aggs: {
+ connections: {
+ composite: {
+ size: 1000,
+ ...afterObj,
+ sources: [
+ { [SERVICE_NAME]: { terms: { field: SERVICE_NAME } } },
+ {
+ [SERVICE_ENVIRONMENT]: {
+ terms: { field: SERVICE_ENVIRONMENT, missing_bucket: true }
+ }
+ },
+ {
+ [SPAN_TYPE]: {
+ terms: { field: SPAN_TYPE, missing_bucket: true }
+ }
+ },
+ {
+ [SPAN_SUBTYPE]: {
+ terms: { field: SPAN_SUBTYPE, missing_bucket: true }
+ }
+ },
+ {
+ [DESTINATION_ADDRESS]: {
+ terms: { field: DESTINATION_ADDRESS }
+ }
+ }
+ ]
+ },
+ aggs: {
+ sample: {
+ sampler: {
+ shard_size: 30
+ },
+ aggs: {
+ trace_ids: {
+ terms: {
+ field: TRACE_ID,
+ execution_hint: 'map' as const,
+ // remove bias towards large traces by sorting on trace.id
+ // which will be random-esque
+ order: {
+ _key: 'desc' as const
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ const tracesSampleResponse = await client.search<
+ { trace: { id: string } },
+ typeof params
+ >(params);
+
+ let nextAfter: string | undefined;
+
+ const receivedAfterKey =
+ tracesSampleResponse.aggregations?.connections.after_key;
+
+ if (!after) {
+ nextAfter = 'top';
+ } else if (receivedAfterKey) {
+ nextAfter = Buffer.from(JSON.stringify(receivedAfterKey)).toString(
+ 'base64'
+ );
+ }
+
+ // make sure at least one trace per composite/connection bucket
+ // is queried
+ const traceIdsWithPriority =
+ tracesSampleResponse.aggregations?.connections.buckets.flatMap(bucket =>
+ bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({
+ traceId: sampleDocBucket.key as string,
+ priority: index
+ }))
+ ) || [];
+
+ const traceIds = take(
+ uniq(
+ sortBy(traceIdsWithPriority, 'priority').map(({ traceId }) => traceId)
+ ),
+ MAX_TRACES_TO_INSPECT
+ );
+
+ return {
+ after: nextAfter,
+ traceIds
+ };
+}
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 acd5dc119b737..bbf2a6882c3c7 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 8e578a839ae56..2f44b9231eae2 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/transactions/breakdown/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts
index f49c1e022a070..870b02fa7ba6d 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: '#5bbaa0',
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 e98842151da84..cf27d20c24360 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 './services';
+import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map';
const createApmApi = () => {
const api = createApi()
@@ -118,10 +118,13 @@ const createApmApi = () => {
.add(transactionsLocalFiltersRoute)
.add(serviceNodesLocalFiltersRoute)
.add(uiFiltersEnvironmentsRoute)
- .add(serviceMapRoute)
// Transaction
- .add(transactionByTraceIdRoute);
+ .add(transactionByTraceIdRoute)
+
+ // Service map
+ .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
new file mode 100644
index 0000000000000..584598805f8b3
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/routes/service_map.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as t from 'io-ts';
+import Boom from 'boom';
+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',
+ params: {
+ query: t.intersection([
+ t.partial({ environment: t.string, serviceName: t.string }),
+ uiFiltersRt,
+ rangeRt,
+ t.partial({ after: t.string })
+ ])
+ },
+ handler: async ({ context, request }) => {
+ if (!context.config['xpack.apm.serviceMapEnabled']) {
+ throw Boom.notFound();
+ }
+ const setup = await setupRequest(context, request);
+ const {
+ query: { serviceName, environment, after }
+ } = context.params;
+ 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/server/routes/services.ts b/x-pack/legacy/plugins/apm/server/routes/services.ts
index 78cb092b85db6..18777183ea1de 100644
--- a/x-pack/legacy/plugins/apm/server/routes/services.ts
+++ b/x-pack/legacy/plugins/apm/server/routes/services.ts
@@ -5,7 +5,6 @@
*/
import * as t from 'io-ts';
-import Boom from 'boom';
import { AgentName } from '../../typings/es_schemas/ui/fields/Agent';
import {
createApmTelementry,
@@ -18,7 +17,6 @@ import { getServiceTransactionTypes } from '../lib/services/get_service_transact
import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadata';
import { createRoute } from './create_route';
import { uiFiltersRt, rangeRt } from './default_api_types';
-import { getServiceMap } from '../lib/services/map';
import { getServiceAnnotations } from '../lib/services/annotations';
export const servicesRoute = createRoute(() => ({
@@ -87,19 +85,6 @@ export const serviceNodeMetadataRoute = createRoute(() => ({
}
}));
-export const serviceMapRoute = createRoute(() => ({
- path: '/api/apm/service-map',
- params: {
- query: rangeRt
- },
- handler: async ({ context }) => {
- if (context.config['xpack.apm.serviceMapEnabled']) {
- return getServiceMap();
- }
- return new Boom('Not found', { statusCode: 404 });
- }
-}));
-
export const serviceAnnotationsRoute = createRoute(() => ({
path: '/api/apm/services/{serviceName}/annotations',
params: {
diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch/aggregations.ts
index 74a9436d7a4bc..6d3620f11a87b 100644
--- a/x-pack/legacy/plugins/apm/typings/elasticsearch/aggregations.ts
+++ b/x-pack/legacy/plugins/apm/typings/elasticsearch/aggregations.ts
@@ -36,6 +36,19 @@ interface MetricsAggregationResponsePart {
value: number | null;
}
+type GetCompositeKeys<
+ TAggregationOptionsMap extends AggregationOptionsMap
+> = TAggregationOptionsMap extends {
+ composite: { sources: Array };
+}
+ ? keyof Source
+ : never;
+
+type CompositeOptionsSource = Record<
+ string,
+ { terms: { field: string; missing_bucket?: boolean } } | undefined
+>;
+
export interface AggregationOptionsByType {
terms: {
field: string;
@@ -97,6 +110,22 @@ export interface AggregationOptionsByType {
buckets_path: BucketsPath;
script?: Script;
};
+ composite: {
+ size?: number;
+ sources: CompositeOptionsSource[];
+ after?: Record;
+ };
+ diversified_sampler: {
+ shard_size?: number;
+ max_docs_per_value?: number;
+ } & ({ script: Script } | { field: string }); // TODO use MetricsAggregationOptions if possible
+ scripted_metric: {
+ params?: Record;
+ init_script?: Script;
+ map_script: Script;
+ combine_script: Script;
+ reduce_script: Script;
+ };
}
type AggregationType = keyof AggregationOptionsByType;
@@ -229,6 +258,24 @@ interface AggregationResponsePart<
value: number | null;
}
| undefined;
+ composite: {
+ after_key: Record, number>;
+ buckets: Array<
+ {
+ key: Record, number>;
+ doc_count: number;
+ } & BucketSubAggregationResponse<
+ TAggregationOptionsMap['aggs'],
+ TDocument
+ >
+ >;
+ };
+ diversified_sampler: {
+ doc_count: number;
+ } & AggregationResponseMap;
+ scripted_metric: {
+ value: unknown;
+ };
}
// Type for debugging purposes. If you see an error in AggregationResponseMap
diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch/index.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch/index.ts
index eff39838bd957..064b684cf9aa6 100644
--- a/x-pack/legacy/plugins/apm/typings/elasticsearch/index.ts
+++ b/x-pack/legacy/plugins/apm/typings/elasticsearch/index.ts
@@ -56,6 +56,7 @@ export interface ESFilter {
| string
| string[]
| number
+ | boolean
| Record
| ESFilter[];
};
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 a1b1a8198bb35..993fac46ad7cb 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 0e07c2b4960b7..29cdcfccfc756 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/expression_types/embeddable.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts
index 063e69d1d2141..e728ea25f5504 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts
@@ -5,11 +5,11 @@
*/
import { ExpressionType } from 'src/plugins/expressions/public';
-import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public';
+import { EmbeddableInput } from '../../../../../../src/plugins/embeddable/public';
import { EmbeddableTypes } from './embeddable_types';
export const EmbeddableExpressionType = 'embeddable';
-export { EmbeddableTypes };
+export { EmbeddableTypes, EmbeddableInput };
export interface EmbeddableExpression {
type: typeof EmbeddableExpressionType;
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts
index 3669bd3e08201..8f5ad859d28ba 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts
@@ -9,7 +9,7 @@ import { MAP_SAVED_OBJECT_TYPE } from '../../../maps/common/constants';
import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/visualize_embeddable/constants';
import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants';
-export const EmbeddableTypes = {
+export const EmbeddableTypes: { map: string; search: string; visualization: string } = {
map: MAP_SAVED_OBJECT_TYPE,
search: SEARCH_EMBEDDABLE_TYPE,
visualization: VISUALIZE_EMBEDDABLE_TYPE,
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts
index 097aef69d4b4c..48b50930d563e 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts
@@ -32,6 +32,7 @@ import { image } from './image';
import { joinRows } from './join_rows';
import { lt } from './lt';
import { lte } from './lte';
+import { mapCenter } from './map_center';
import { mapColumn } from './mapColumn';
import { math } from './math';
import { metric } from './metric';
@@ -57,6 +58,7 @@ import { staticColumn } from './staticColumn';
import { string } from './string';
import { table } from './table';
import { tail } from './tail';
+import { timerange } from './time_range';
import { timefilter } from './timefilter';
import { timefilterControl } from './timefilterControl';
import { switchFn } from './switch';
@@ -91,6 +93,7 @@ export const functions = [
lt,
lte,
joinRows,
+ mapCenter,
mapColumn,
math,
metric,
@@ -118,6 +121,7 @@ export const functions = [
tail,
timefilter,
timefilterControl,
+ timerange,
switchFn,
caseFn,
];
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.ts
new file mode 100644
index 0000000000000..21f9e9fe3148d
--- /dev/null
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/map_center.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { ExpressionFunction } from 'src/plugins/expressions/common';
+import { getFunctionHelp } from '../../../i18n/functions';
+import { MapCenter } from '../../../types';
+
+interface Args {
+ lat: number;
+ lon: number;
+ zoom: number;
+}
+
+export function mapCenter(): ExpressionFunction<'mapCenter', null, Args, MapCenter> {
+ const { help, args: argHelp } = getFunctionHelp().mapCenter;
+ return {
+ name: 'mapCenter',
+ help,
+ type: 'mapCenter',
+ context: {
+ types: ['null'],
+ },
+ args: {
+ lat: {
+ types: ['number'],
+ required: true,
+ help: argHelp.lat,
+ },
+ lon: {
+ types: ['number'],
+ required: true,
+ help: argHelp.lon,
+ },
+ zoom: {
+ types: ['number'],
+ required: true,
+ help: argHelp.zoom,
+ },
+ },
+ fn: (context, args) => {
+ return {
+ type: 'mapCenter',
+ ...args,
+ };
+ },
+ };
+}
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts
index 25f035bbb6d8c..5b95886faa13d 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts
@@ -5,7 +5,7 @@
*/
jest.mock('ui/new_platform');
import { savedMap } from './saved_map';
-import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters';
+import { getQueryFilters } from '../../../server/lib/build_embeddable_filters';
const filterContext = {
and: [
@@ -24,20 +24,22 @@ describe('savedMap', () => {
const fn = savedMap().fn;
const args = {
id: 'some-id',
+ center: null,
+ title: null,
+ timerange: null,
+ hideLayer: [],
};
it('accepts null context', () => {
const expression = fn(null, args, {});
expect(expression.input.filters).toEqual([]);
- expect(expression.input.timeRange).toBeUndefined();
});
it('accepts filter context', () => {
const expression = fn(filterContext, args, {});
- const embeddableFilters = buildEmbeddableFilters(filterContext.and);
+ const embeddableFilters = getQueryFilters(filterContext.and);
- expect(expression.input.filters).toEqual(embeddableFilters.filters);
- expect(expression.input.timeRange).toEqual(embeddableFilters.timeRange);
+ expect(expression.input.filters).toEqual(embeddableFilters);
});
});
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts
index 460cb9c34efff..b6d88c06ed06d 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts
@@ -7,8 +7,8 @@
import { ExpressionFunction } from 'src/plugins/expressions/common/types';
import { TimeRange } from 'src/plugins/data/public';
import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public';
-import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters';
-import { Filter } from '../../../types';
+import { getQueryFilters } from '../../../server/lib/build_embeddable_filters';
+import { Filter, MapCenter, TimeRange as TimeRangeArg } from '../../../types';
import {
EmbeddableTypes,
EmbeddableExpressionType,
@@ -19,19 +19,36 @@ import { esFilters } from '../../../../../../../src/plugins/data/public';
interface Arguments {
id: string;
+ center: MapCenter | null;
+ hideLayer: string[];
+ title: string | null;
+ timerange: TimeRangeArg | null;
}
// Map embeddable is missing proper typings, so type is just to document what we
// are expecting to pass to the embeddable
-interface SavedMapInput extends EmbeddableInput {
+export type SavedMapInput = EmbeddableInput & {
id: string;
+ isLayerTOCOpen: boolean;
timeRange?: TimeRange;
refreshConfig: {
isPaused: boolean;
interval: number;
};
+ hideFilterActions: true;
filters: esFilters.Filter[];
-}
+ mapCenter?: {
+ lat: number;
+ lon: number;
+ zoom: number;
+ };
+ hiddenLayers?: string[];
+};
+
+const defaultTimeRange = {
+ from: 'now-15m',
+ to: 'now',
+};
type Return = EmbeddableExpression;
@@ -46,21 +63,56 @@ export function savedMap(): ExpressionFunction<'savedMap', Filter | null, Argume
required: false,
help: argHelp.id,
},
+ center: {
+ types: ['mapCenter'],
+ help: argHelp.center,
+ required: false,
+ },
+ hideLayer: {
+ types: ['string'],
+ help: argHelp.hideLayer,
+ required: false,
+ multi: true,
+ },
+ timerange: {
+ types: ['timerange'],
+ help: argHelp.timerange,
+ required: false,
+ },
+ title: {
+ types: ['string'],
+ help: argHelp.title,
+ required: false,
+ },
},
type: EmbeddableExpressionType,
- fn: (context, { id }) => {
+ fn: (context, args) => {
const filters = context ? context.and : [];
+ const center = args.center
+ ? {
+ lat: args.center.lat,
+ lon: args.center.lon,
+ zoom: args.center.zoom,
+ }
+ : undefined;
+
return {
type: EmbeddableExpressionType,
input: {
- id,
- ...buildEmbeddableFilters(filters),
-
+ id: args.id,
+ filters: getQueryFilters(filters),
+ timeRange: args.timerange || defaultTimeRange,
refreshConfig: {
isPaused: false,
interval: 0,
},
+
+ mapCenter: center,
+ hideFilterActions: true,
+ title: args.title ? args.title : undefined,
+ isLayerTOCOpen: false,
+ hiddenLayers: args.hideLayer || [],
},
embeddableType: EmbeddableTypes.map,
};
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts
new file mode 100644
index 0000000000000..716026279ccea
--- /dev/null
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/time_range.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ExpressionFunction } from 'src/plugins/expressions/common';
+import { getFunctionHelp } from '../../../i18n/functions';
+import { TimeRange } from '../../../types';
+
+interface Args {
+ from: string;
+ to: string;
+}
+
+export function timerange(): ExpressionFunction<'timerange', null, Args, TimeRange> {
+ const { help, args: argHelp } = getFunctionHelp().timerange;
+ return {
+ name: 'timerange',
+ help,
+ type: 'timerange',
+ context: {
+ types: ['null'],
+ },
+ args: {
+ from: {
+ types: ['string'],
+ required: true,
+ help: argHelp.from,
+ },
+ to: {
+ types: ['string'],
+ required: true,
+ help: argHelp.to,
+ },
+ },
+ fn: (context, args) => {
+ return {
+ type: 'timerange',
+ ...args,
+ };
+ },
+ };
+}
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
similarity index 74%
rename from x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx
rename to x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
index 5c7ef1a8c1799..8642ebd901bb4 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
@@ -10,32 +10,27 @@ import { I18nContext } from 'ui/i18n';
import { npStart } from 'ui/new_platform';
import {
IEmbeddable,
+ EmbeddableFactory,
EmbeddablePanel,
EmbeddableFactoryNotFoundError,
- EmbeddableInput,
-} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
-import { start } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
-import { EmbeddableExpression } from '../expression_types/embeddable';
-import { RendererStrings } from '../../i18n';
+} from '../../../../../../../src/plugins/embeddable/public';
+import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
+import { EmbeddableExpression } from '../../expression_types/embeddable';
+import { RendererStrings } from '../../../i18n';
import {
SavedObjectFinderProps,
SavedObjectFinderUi,
-} from '../../../../../../src/plugins/kibana_react/public';
+} from '../../../../../../../src/plugins/kibana_react/public';
const { embeddable: strings } = RendererStrings;
+import { embeddableInputToExpression } from './embeddable_input_to_expression';
+import { EmbeddableInput } from '../../expression_types';
+import { RendererHandlers } from '../../../types';
const embeddablesRegistry: {
[key: string]: IEmbeddable;
} = {};
-interface Handlers {
- setFilter: (text: string) => void;
- getFilter: () => string | null;
- done: () => void;
- onResize: (fn: () => void) => void;
- onDestroy: (fn: () => void) => void;
-}
-
const renderEmbeddable = (embeddableObject: IEmbeddable, domNode: HTMLElement) => {
const SavedObjectFinder = (props: SavedObjectFinderProps) => (
({
render: async (
domNode: HTMLElement,
{ input, embeddableType }: EmbeddableExpression,
- handlers: Handlers
+ handlers: RendererHandlers
) => {
if (!embeddablesRegistry[input.id]) {
const factory = Array.from(start.getEmbeddableFactories()).find(
embeddableFactory => embeddableFactory.type === embeddableType
- );
+ ) as EmbeddableFactory;
if (!factory) {
handlers.done();
@@ -86,8 +81,13 @@ const embeddable = () => ({
}
const embeddableObject = await factory.createFromSavedObject(input.id, input);
+
embeddablesRegistry[input.id] = embeddableObject;
+ ReactDOM.unmountComponentAtNode(domNode);
+ const subscription = embeddableObject.getInput$().subscribe(function(updatedInput) {
+ handlers.onEmbeddableInputChange(embeddableInputToExpression(updatedInput, embeddableType));
+ });
ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => handlers.done());
handlers.onResize(() => {
@@ -97,7 +97,11 @@ const embeddable = () => ({
});
handlers.onDestroy(() => {
+ subscription.unsubscribe();
+ handlers.onEmbeddableDestroyed();
+
delete embeddablesRegistry[input.id];
+
return ReactDOM.unmountComponentAtNode(domNode);
});
} else {
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts
new file mode 100644
index 0000000000000..93d747537c34c
--- /dev/null
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts
@@ -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 { embeddableInputToExpression } from './embeddable_input_to_expression';
+import { SavedMapInput } from '../../functions/common/saved_map';
+import { EmbeddableTypes } from '../../expression_types';
+import { fromExpression, Ast } from '@kbn/interpreter/common';
+
+const baseSavedMapInput = {
+ id: 'embeddableId',
+ filters: [],
+ isLayerTOCOpen: false,
+ refreshConfig: {
+ isPaused: true,
+ interval: 0,
+ },
+ hideFilterActions: true as true,
+};
+
+describe('input to expression', () => {
+ describe('Map Embeddable', () => {
+ it('converts to a savedMap expression', () => {
+ const input: SavedMapInput = {
+ ...baseSavedMapInput,
+ };
+
+ const expression = embeddableInputToExpression(input, EmbeddableTypes.map);
+ const ast = fromExpression(expression);
+
+ expect(ast.type).toBe('expression');
+ expect(ast.chain[0].function).toBe('savedMap');
+
+ expect(ast.chain[0].arguments.id).toStrictEqual([input.id]);
+
+ expect(ast.chain[0].arguments).not.toHaveProperty('title');
+ expect(ast.chain[0].arguments).not.toHaveProperty('center');
+ expect(ast.chain[0].arguments).not.toHaveProperty('timerange');
+ });
+
+ it('includes optional input values', () => {
+ const input: SavedMapInput = {
+ ...baseSavedMapInput,
+ mapCenter: {
+ lat: 1,
+ lon: 2,
+ zoom: 3,
+ },
+ title: 'title',
+ timeRange: {
+ from: 'now-1h',
+ to: 'now',
+ },
+ };
+
+ const expression = embeddableInputToExpression(input, EmbeddableTypes.map);
+ const ast = fromExpression(expression);
+
+ const centerExpression = ast.chain[0].arguments.center[0] as Ast;
+
+ expect(centerExpression.chain[0].function).toBe('mapCenter');
+ expect(centerExpression.chain[0].arguments.lat[0]).toEqual(input.mapCenter?.lat);
+ expect(centerExpression.chain[0].arguments.lon[0]).toEqual(input.mapCenter?.lon);
+ expect(centerExpression.chain[0].arguments.zoom[0]).toEqual(input.mapCenter?.zoom);
+
+ const timerangeExpression = ast.chain[0].arguments.timerange[0] as Ast;
+
+ expect(timerangeExpression.chain[0].function).toBe('timerange');
+ expect(timerangeExpression.chain[0].arguments.from[0]).toEqual(input.timeRange?.from);
+ expect(timerangeExpression.chain[0].arguments.to[0]).toEqual(input.timeRange?.to);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts
new file mode 100644
index 0000000000000..a3cb53acebed2
--- /dev/null
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { EmbeddableTypes, EmbeddableInput } from '../../expression_types';
+import { SavedMapInput } from '../../functions/common/saved_map';
+
+/*
+ Take the input from an embeddable and the type of embeddable and convert it into an expression
+*/
+export function embeddableInputToExpression(
+ input: EmbeddableInput,
+ embeddableType: string
+): string {
+ const expressionParts: string[] = [];
+
+ if (embeddableType === EmbeddableTypes.map) {
+ const mapInput = input as SavedMapInput;
+
+ expressionParts.push('savedMap');
+
+ expressionParts.push(`id="${input.id}"`);
+
+ if (input.title) {
+ expressionParts.push(`title="${input.title}"`);
+ }
+
+ if (mapInput.mapCenter) {
+ expressionParts.push(
+ `center={mapCenter lat=${mapInput.mapCenter.lat} lon=${mapInput.mapCenter.lon} zoom=${mapInput.mapCenter.zoom}}`
+ );
+ }
+
+ if (mapInput.timeRange) {
+ expressionParts.push(
+ `timerange={timerange from="${mapInput.timeRange.from}" to="${mapInput.timeRange.to}"}`
+ );
+ }
+
+ if (mapInput.hiddenLayers && mapInput.hiddenLayers.length) {
+ for (const layerId of mapInput.hiddenLayers) {
+ expressionParts.push(`hideLayer="${layerId}"`);
+ }
+ }
+ }
+
+ return expressionParts.join(' ');
+}
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js
index 50fa6943fc74a..48364be06e539 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js
@@ -7,7 +7,7 @@
import { advancedFilter } from './advanced_filter';
import { debug } from './debug';
import { dropdownFilter } from './dropdown_filter';
-import { embeddable } from './embeddable';
+import { embeddable } from './embeddable/embeddable';
import { error } from './error';
import { image } from './image';
import { markdown } from './markdown';
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 69f584af41556..d60dc13f0105b 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 c056e7d1f2281..baa2127b03c3c 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 de19d3e29221b..bcad4678e0b6a 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/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts
index c898db7467b44..d0a9051d7af87 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/i18n/functions/dict/map_center.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/map_center.ts
new file mode 100644
index 0000000000000..3022ad07089d2
--- /dev/null
+++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/map_center.ts
@@ -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 { i18n } from '@kbn/i18n';
+import { mapCenter } from '../../../canvas_plugin_src/functions/common/map_center';
+import { FunctionHelp } from '../';
+import { FunctionFactory } from '../../../types';
+
+export const help: FunctionHelp> = {
+ help: i18n.translate('xpack.canvas.functions.mapCenterHelpText', {
+ defaultMessage: `Returns an object with the center coordinates and zoom level of the map`,
+ }),
+ args: {
+ lat: i18n.translate('xpack.canvas.functions.mapCenter.args.latHelpText', {
+ defaultMessage: `Latitude for the center of the map`,
+ }),
+ lon: i18n.translate('xpack.canvas.functions.savedMap.args.lonHelpText', {
+ defaultMessage: `Longitude for the center of the map`,
+ }),
+ zoom: i18n.translate('xpack.canvas.functions.savedMap.args.zoomHelpText', {
+ defaultMessage: `The zoom level of the map`,
+ }),
+ },
+};
diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_map.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_map.ts
index d01b77e1cfd51..53bcd481f185f 100644
--- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_map.ts
+++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_map.ts
@@ -14,6 +14,20 @@ export const help: FunctionHelp> = {
defaultMessage: `Returns an embeddable for a saved map object`,
}),
args: {
- id: 'The id of the saved map object',
+ id: i18n.translate('xpack.canvas.functions.savedMap.args.idHelpText', {
+ defaultMessage: `The ID of the Saved Map Object`,
+ }),
+ center: i18n.translate('xpack.canvas.functions.savedMap.args.centerHelpText', {
+ defaultMessage: `The center and zoom level the map should have`,
+ }),
+ hideLayer: i18n.translate('xpack.canvas.functions.savedMap.args.hideLayer', {
+ defaultMessage: `The IDs of map layers that should be hidden`,
+ }),
+ timerange: i18n.translate('xpack.canvas.functions.savedMap.args.timerangeHelpText', {
+ defaultMessage: `The timerange of data that should be included`,
+ }),
+ title: i18n.translate('xpack.canvas.functions.savedMap.args.titleHelpText', {
+ defaultMessage: `The title for the map`,
+ }),
},
};
diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/time_range.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/time_range.ts
new file mode 100644
index 0000000000000..476a9978800df
--- /dev/null
+++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/time_range.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { timerange } from '../../../canvas_plugin_src/functions/common/time_range';
+import { FunctionHelp } from '../function_help';
+import { FunctionFactory } from '../../../types';
+
+export const help: FunctionHelp