diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc
index bb7222020180c..d9502e4cb47ee 100644
--- a/docs/developer/architecture/code-exploration.asciidoc
+++ b/docs/developer/architecture/code-exploration.asciidoc
@@ -86,9 +86,9 @@ Contains the Discover application and the saved search embeddable.
Embeddables are re-usable widgets that can be rendered in any environment or plugin. Developers can embed them directly in their plugin. End users can dynamically add them to any embeddable containers.
-- {kib-repo}blob/{branch}/src/plugins/es_ui_shared[esUiShared]
+- {kib-repo}blob/{branch}/src/plugins/es_ui_shared/README.md[esUiShared]
-WARNING: Missing README.
+This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module.
- {kib-repo}blob/{branch}/src/plugins/expressions/README.md[expressions]
diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md
index 22f70ce22b574..478ba2d409acd 100644
--- a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md
+++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md
@@ -9,8 +9,10 @@ Creates [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_
Signature:
```typescript
-createKbnUrlStateStorage: ({ useHash, history }?: {
+createKbnUrlStateStorage: ({ useHash, history, onGetError, onSetError, }?: {
useHash: boolean;
history?: History | undefined;
+ onGetError?: ((error: Error) => void) | undefined;
+ onSetError?: ((error: Error) => void) | undefined;
}) => IKbnUrlStateStorage
```
diff --git a/docs/drilldowns/explore-underlying-data.asciidoc b/docs/drilldowns/explore-underlying-data.asciidoc
index e0f940f73e96e..c2bba599730d8 100644
--- a/docs/drilldowns/explore-underlying-data.asciidoc
+++ b/docs/drilldowns/explore-underlying-data.asciidoc
@@ -33,9 +33,9 @@ applies the filters and time range created by the events that triggered the acti
[role="screenshot"]
image::images/explore_data_in_chart.png[Explore underlying data from chart]
-You can disable this action by adding the following line to your `kibana.yml` config.
+To enable this action add the following line to your `kibana.yml` config.
["source","yml"]
-----------
-xpack.discoverEnhanced.actions.exploreDataInChart.enabled: false
+xpack.discoverEnhanced.actions.exploreDataInChart.enabled: true
-----------
diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc
index 07c0bfcf35cb7..51470513198b9 100644
--- a/docs/glossary.asciidoc
+++ b/docs/glossary.asciidoc
@@ -214,6 +214,13 @@ syslog, Apache, and other webserver logs. See
[[k_glos]]
== K
+[[glossary-kibana-privileges]] {kib} privileges ::
+// tag::kibana-privileges-def[]
+Enable administrators to grant users read-only, read-write, or no access to
+individual features within <> in {kib}. See
+{kibana-ref}/kibana-privileges.html[{kib} privileges].
+// end::kibana-privileges-def[]
+
[[glossary-kql]] {kib} Query Language (KQL) ::
// tag::kql-def[]
The default language for querying in {kib}. KQL provides
diff --git a/package.json b/package.json
index aaa7ae7ee4684..fc3af14ecae09 100644
--- a/package.json
+++ b/package.json
@@ -117,9 +117,9 @@
]
},
"dependencies": {
- "@babel/core": "^7.10.2",
- "@babel/plugin-transform-modules-commonjs": "^7.10.1",
- "@babel/register": "^7.10.1",
+ "@babel/core": "^7.11.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.10.4",
+ "@babel/register": "^7.10.5",
"@elastic/apm-rum": "^5.2.0",
"@elastic/charts": "19.8.1",
"@elastic/datemath": "5.0.3",
@@ -159,7 +159,6 @@
"bluebird": "3.5.5",
"boom": "^7.2.0",
"brace": "0.11.1",
- "browserslist-useragent": "^3.0.2",
"cache-loader": "^4.1.0",
"chalk": "^2.4.2",
"check-disk-space": "^2.1.0",
@@ -290,8 +289,8 @@
"yauzl": "2.10.0"
},
"devDependencies": {
- "@babel/parser": "^7.10.2",
- "@babel/types": "^7.10.2",
+ "@babel/parser": "^7.11.2",
+ "@babel/types": "^7.11.0",
"@elastic/eslint-config-kibana": "0.15.0",
"@elastic/eslint-plugin-eui": "0.0.2",
"@elastic/github-checks-reporter": "0.0.20b3",
@@ -319,7 +318,6 @@
"@types/babel__core": "^7.1.2",
"@types/bluebird": "^3.1.1",
"@types/boom": "^7.2.0",
- "@types/browserslist-useragent": "^3.0.0",
"@types/chance": "^1.0.0",
"@types/cheerio": "^0.22.10",
"@types/chromedriver": "^81.0.0",
diff --git a/packages/elastic-datemath/package.json b/packages/elastic-datemath/package.json
index 15040a6243ff2..ad4190f981439 100644
--- a/packages/elastic-datemath/package.json
+++ b/packages/elastic-datemath/package.json
@@ -11,8 +11,8 @@
"kbn:watch": "yarn build --watch"
},
"devDependencies": {
- "@babel/cli": "^7.10.1",
- "@babel/preset-env": "^7.10.2",
+ "@babel/cli": "^7.10.5",
+ "@babel/preset-env": "^7.11.0",
"babel-plugin-add-module-exports": "^1.0.2",
"moment": "^2.24.0"
},
diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json
index bd3f5832b7140..873252ceb0a1a 100644
--- a/packages/kbn-analytics/package.json
+++ b/packages/kbn-analytics/package.json
@@ -14,7 +14,7 @@
"kbn:watch": "node scripts/build --source-maps --watch"
},
"devDependencies": {
- "@babel/cli": "^7.10.1",
+ "@babel/cli": "^7.10.5",
"@kbn/dev-utils": "1.0.0",
"@kbn/babel-preset": "1.0.0",
"typescript": "3.9.5"
diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json
index 83530beffd2b2..db1f2161b6e38 100644
--- a/packages/kbn-babel-preset/package.json
+++ b/packages/kbn-babel-preset/package.json
@@ -4,14 +4,14 @@
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
- "@babel/plugin-proposal-class-properties": "^7.10.1",
- "@babel/plugin-proposal-export-namespace-from": "^7.10.1",
- "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1",
- "@babel/plugin-proposal-optional-chaining": "^7.10.1",
- "@babel/plugin-proposal-private-methods": "^7.10.1",
- "@babel/preset-env": "^7.10.2",
- "@babel/preset-react": "^7.10.1",
- "@babel/preset-typescript": "^7.10.1",
+ "@babel/plugin-proposal-class-properties": "^7.10.4",
+ "@babel/plugin-proposal-export-namespace-from": "^7.10.4",
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
+ "@babel/plugin-proposal-optional-chaining": "^7.11.0",
+ "@babel/plugin-proposal-private-methods": "^7.10.4",
+ "@babel/preset-env": "^7.11.0",
+ "@babel/preset-react": "^7.10.4",
+ "@babel/preset-typescript": "^7.10.4",
"babel-plugin-add-module-exports": "^1.0.2",
"babel-plugin-filter-imports": "^3.0.0",
"babel-plugin-styled-components": "^1.10.7",
diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json
index c5da144688c3c..0f830acb284a0 100644
--- a/packages/kbn-i18n/package.json
+++ b/packages/kbn-i18n/package.json
@@ -12,8 +12,8 @@
"kbn:watch": "node scripts/build --watch --source-maps"
},
"devDependencies": {
- "@babel/cli": "^7.10.1",
- "@babel/core": "^7.10.2",
+ "@babel/cli": "^7.10.5",
+ "@babel/core": "^7.11.1",
"@kbn/babel-preset": "1.0.0",
"@kbn/dev-utils": "1.0.0",
"@types/intl-relativeformat": "^2.1.0",
diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json
index c6bb06e68b9c0..aef63229ebe96 100644
--- a/packages/kbn-interpreter/package.json
+++ b/packages/kbn-interpreter/package.json
@@ -9,16 +9,16 @@
"kbn:watch": "node scripts/build --dev --watch"
},
"dependencies": {
- "@babel/runtime": "^7.10.2",
+ "@babel/runtime": "^7.11.2",
"@kbn/i18n": "1.0.0",
"lodash": "^4.17.15",
"uuid": "3.3.2"
},
"devDependencies": {
- "@babel/cli": "^7.10.1",
- "@babel/core": "^7.10.2",
- "@babel/plugin-transform-modules-commonjs": "^7.10.1",
- "@babel/plugin-transform-runtime": "^7.10.1",
+ "@babel/cli": "^7.10.5",
+ "@babel/core": "^7.11.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.10.4",
+ "@babel/plugin-transform-runtime": "^7.11.0",
"@kbn/babel-preset": "1.0.0",
"@kbn/dev-utils": "1.0.0",
"babel-loader": "^8.0.6",
diff --git a/packages/kbn-optimizer/README.md b/packages/kbn-optimizer/README.md
index 5d5c5e3b6eb74..13be836f0ea88 100644
--- a/packages/kbn-optimizer/README.md
+++ b/packages/kbn-optimizer/README.md
@@ -10,9 +10,9 @@ The [Webpack config][WebpackConfig] is designed to provide the majority of what
Source maps are enabled except when building the distributable. They show the code actually being executed by the browser to strike a balance between debuggability and performance. They are not configurable at this time but will be configurable once we have a developer configuration solution that doesn't rely on the server (see [#55656](https://github.com/elastic/kibana/issues/55656)).
-### IE Support
+### Browser Support
-To make front-end code easier to debug the optimizer uses the `BROWSERSLIST_ENV=dev` environment variable (by default) to build JS and CSS that is compatible with modern browsers. In order to support older browsers like IE in development you will need to specify the `BROWSERSLIST_ENV=production` environment variable or build a distributable for testing.
+To make front-end code easier to debug the optimizer uses the `BROWSERSLIST_ENV=dev` environment variable (by default) to build JS and CSS that is compatible with modern browsers. In order to support all browsers that we support with the distributable you will need to specify the `BROWSERSLIST_ENV=production` environment variable or build a distributable for testing.
## Running the optimizer
diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json
index e6eb5de31abd8..84e5c79e2e358 100644
--- a/packages/kbn-optimizer/package.json
+++ b/packages/kbn-optimizer/package.json
@@ -10,7 +10,7 @@
"kbn:watch": "yarn build --watch"
},
"dependencies": {
- "@babel/cli": "^7.10.1",
+ "@babel/cli": "^7.10.5",
"@kbn/babel-preset": "1.0.0",
"@kbn/dev-utils": "1.0.0",
"@kbn/ui-shared-deps": "1.0.0",
diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json
index f370265876df3..45582ad2af97a 100644
--- a/packages/kbn-plugin-helpers/package.json
+++ b/packages/kbn-plugin-helpers/package.json
@@ -12,7 +12,7 @@
"plugin-helpers": "bin/plugin-helpers.js"
},
"dependencies": {
- "@babel/core": "^7.10.2",
+ "@babel/core": "^7.11.1",
"argv-split": "^2.0.1",
"commander": "^3.0.0",
"del": "^5.1.0",
diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json
index 188db0a8321a2..3e40bf40222e6 100644
--- a/packages/kbn-pm/package.json
+++ b/packages/kbn-pm/package.json
@@ -10,11 +10,11 @@
"prettier": "prettier --write './src/**/*.ts'"
},
"devDependencies": {
- "@babel/core": "^7.10.2",
- "@babel/plugin-proposal-class-properties": "^7.10.1",
- "@babel/plugin-proposal-object-rest-spread": "^7.10.1",
- "@babel/preset-env": "^7.10.2",
- "@babel/preset-typescript": "^7.10.1",
+ "@babel/core": "^7.11.1",
+ "@babel/plugin-proposal-class-properties": "^7.10.4",
+ "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
+ "@babel/preset-env": "^7.11.0",
+ "@babel/preset-typescript": "^7.10.4",
"@types/cmd-shim": "^2.0.0",
"@types/cpy": "^5.1.0",
"@types/dedent": "^0.7.0",
diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json
index 38e4668fc1e42..9482ea83cc257 100644
--- a/packages/kbn-test/package.json
+++ b/packages/kbn-test/package.json
@@ -10,7 +10,7 @@
"kbn:watch": "yarn build --watch"
},
"devDependencies": {
- "@babel/cli": "^7.10.1",
+ "@babel/cli": "^7.10.5",
"@kbn/babel-preset": "1.0.0",
"@kbn/dev-utils": "1.0.0",
"@types/joi": "^13.4.2",
diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json
index 7933ce06d6847..a095d9ac2a77f 100644
--- a/packages/kbn-ui-framework/package.json
+++ b/packages/kbn-ui-framework/package.json
@@ -30,7 +30,7 @@
"enzyme-adapter-react-16": "^1.9.1"
},
"devDependencies": {
- "@babel/core": "^7.10.2",
+ "@babel/core": "^7.11.1",
"@elastic/eui": "0.0.55",
"@kbn/babel-preset": "1.0.0",
"@kbn/optimizer": "1.0.0",
diff --git a/rfcs/text/0012_encryption_key_rotation.md b/rfcs/text/0012_encryption_key_rotation.md
new file mode 100644
index 0000000000000..d984d1157a0a1
--- /dev/null
+++ b/rfcs/text/0012_encryption_key_rotation.md
@@ -0,0 +1,119 @@
+- Start Date: 2020-07-22
+- RFC PR: [#72828](https://github.com/elastic/kibana/pull/72828)
+- Kibana Issue: (leave this empty)
+
+# Summary
+
+This RFC proposes a way of the encryption key (`xpack.encryptedSavedObjects.encryptionKey`) rotation that would allow administrators to seamlessly change existing encryption key without any data loss and manual intervention.
+
+# Basic example
+
+When administrators decide to rotate encryption key they will have to generate a new one and move the old key(s) to the `keyRotation` section in the `kibana.yml`:
+
+```yaml
+xpack.encryptedSavedObjects:
+ encryptionKey: "NEW-encryption-key"
+ keyRotation:
+ decryptionOnlyKeys: ["OLD-encryption-key-1", "OLD-encryption-key-2"]
+```
+
+Before old decryption-only key is disposed administrators may want to call a dedicated and _protected_ API endpoint that will go through all registered Saved Objects with encrypted attributes and try to re-encrypt them with the primary encryption key:
+
+```http request
+POST https://localhost:5601/api/encrypted_saved_objects/rotate_key?conflicts=abort
+Content-Type: application/json
+Kbn-Xsrf: true
+```
+
+# Motivation
+
+Today when encryption key changes we can no longer decrypt Saved Objects attributes that were previously encrypted with the `EncryptedSavedObjects` plugin. We handle this case in two different ways depending on whether consumers explicitly requested decryption or not:
+
+* If consumers explicitly request decryption via `getDecryptedAsInternalUser()` we abort operation and throw exception.
+* If consumers fetch Saved Objects with encrypted attributes that should be automatically decrypted (the ones with `dangerouslyExposeValue: true` marker) via standard Saved Objects APIs we don't abort operation, but rather strip all encrypted attributes from the response and record decryption error in the `error` Saved Object field.
+* If Kibana tries to migrate encrypted Saved Objects at the start up time we abort operation and throw exception.
+
+In both of these cases we throw or record error with the specific type to allow consumers to gracefully handle this scenario and either drop Saved Objects with unrecoverable encrypted attributes or facilitate the process of re-entering and re-encryption of the new values.
+
+This approach works reasonably well in some scenarios, but it may become very troublesome if we have to deal with lots of Saved Objects. Moreover, we'd like to recommend our users to periodically rotate encryption keys even if they aren't compromised. Hence, we need to provide a way of seamless migration of the existing encrypted Saved Objects to a new encryption key.
+
+There are two main scenarios we'd like to cover in this RFC:
+
+## Encryption key is not available
+
+Administrators may lose existing encryption key or explicitly decide to not use it if it was compromised and users can no longer trust encrypted content that may have been tampered with. In this scenario encrypted portion of the existing Saved Objects is considered lost, and the only way to recover from this state is a manual intervention described previously. That means `EncryptedSavedObjects` plugin consumers __should__ continue supporting this scenario even after we implement a proper encryption key rotation mechanism described in this RFC.
+
+## Encryption key is available, but needs to be rotated
+
+In this scenario a new encryption key (primary encryption key) will be generated, and we will use it to encrypt new or updated Saved Objects. We will still need to know the old encryption key to decrypt existing attributes, but we will no longer use this key to encrypt any of the new or existing Saved Objects. It's also should be possible to have multiple old decryption-only keys.
+
+The old old decryption-only keys should be eventually disposed and users should have a way to make sure all existing Saved Objects are re-encrypted with the new primary encryption key.
+
+__NOTE:__ users can get into a state when different Saved Objects are encrypted with different encryption keys even if they didn't intend to rotate the encryption key. We anticipate that it can happen during initial Elastic Stack HA setup, when by mistake or intentionally different Kibana instances were using different encryption keys. Key rotation mechanism can help to fix this issue without a data loss.
+
+# Detailed design
+
+The core idea is that when the encryption key needs to be rotated then a new key is generated and becomes a primary one, and the old one moves to the `keyRotation` section:
+
+```yaml
+xpack.encryptedSavedObjects:
+ encryptionKey: "NEW-encryption-key"
+ keyRotation:
+ decryptionOnlyKeys: ["OLD-encryption-key"]
+```
+
+As the name implies, the key from the `decryptionOnlyKeys` is only used to decrypt content that we cannot decrypt with the primary encryption key. It's allowed to have multiple decryption-only keys at the same time. When user creates a new Saved Object or updates the existing one then its content is always encrypted with the primary encryption key. Config schema won't allow having the same key in `encryptionKey` and `decryptionOnlyKeys`.
+
+Having multiple decryption keys at the same time brings one problem though: we need to figure out which key to use to decrypt specific Saved Object. If our encryption keys could have a unique ID that we would store together with the encrypted data (we cannot use encryption key hash for that for obvious reasons) we could know for sure which key to use, but we don't have such functionality right now and it may not be the easiest one to manage through `yml` configuration anyway.
+
+Instead, this RFC proposes to try available existing decryption keys one by one to decrypt Saved Object and always start from the primary one. This way we won't incur any penalty while decrypting Saved Objects that are already encrypted with the primary encryption key, but there will still be some cost when we have to perform multiple decryption attempts. See the [`Drawbacks`](#drawbacks) section for the details.
+
+Technically just having `decryptionOnlyKeys` would be enough to cover the majority of the use cases, but the old decryption-only keys should be eventually disposed. At this point administrators would like to make sure _all_ Saved Objects are encrypted with the new primary encryption key. Another reason to re-encrypt all existing Saved Objects with the new key at once is to preventively reduce the performance impact of the multiple decryption attempts.
+
+We'd like to make this process as simple as possible while meeting the following requirements:
+
+* It should not be required to restart Kibana to perform this type of migration since Saved Objects encrypted with the another encryption key can theoretically appear at any point in time.
+* It should be possible to integrate this operation into other operational flows our users may have and any user-friendly key management UIs we may introduce in this future.
+* Any possible failures that may happen during this operation shouldn't make Kibana nonfunctional.
+* Ordinary users should not be able to trigger this migration since it may consume a considerable amount of computing resources.
+
+We think that the best option we have right now is a dedicated API endpoint that would trigger this migration:
+
+```http request
+POST https://localhost:5601/api/encrypted_saved_objects/rotate_key?conflicts=abort
+Content-Type: application/json
+Kbn-Xsrf: true
+```
+
+This will be a protected endpoint and only user with enough privileges will be able to use it.
+
+Under the hood we'll scroll over all Saved Objects that are registered with `EncryptedSavedObjects` plugin and re-encrypt attributes only for those of them that can only be decrypted with any of the old decryption-only keys. Saved Objects that can be decrypted with the primary encryption key will be ignored. We'll also ignore the ones that cannot be decrypted with any of the available decryption keys at all, and presumably return their IDs in the response.
+
+As for any other encryption or decryption operation we'll record relevant bits in the audit logs.
+
+# Benefits
+
+* The concept of decryption-only keys is easy to grasp and allows Kibana to function even if it has a mix of Saved Objects encrypted with different encryption keys.
+* Support of the key rotation out of the box decreases the chances of the data loss and makes `EncryptedSavedObjects` story more secure and approachable overall.
+
+# Drawbacks
+
+* Multiple decryption attempts affect performance. See [the performance test results](https://github.com/elastic/kibana/pull/72420#issue-453400211) for more details, but making two decryption attempts is basically twice as slow as with a single attempt. Although it's only relevant for the encrypted Saved Objects migration performed at the start up time and batch operations that trigger automatic decryption (only for the Saved Objects registered with `dangerouslyExposeValue: true` marker that nobody is using in Kibana right now), we may have more use cases in the future.
+* Historically we supported Kibana features with either configuration or dedicated UI, but in this case we want to introduce an API endpoint that _should be_ used directly. We may have a key management UI in the future though.
+
+# Alternatives
+
+We cannot think of any better alternative for `decryptionOnlyKeys` at the moment, but instead of API endpoint for the batch re-encryption we could potentially use another `kibana.yml` config option. For example `keyRotation.mode: onWrite | onStart | both`, but it feels a bit hacky and cannot be really integrated with anything else.
+
+# Adoption strategy
+
+Adoption strategy is pretty straightforward since the feature is an enhancement and doesn't bring any BWC concerns.
+
+# How we teach this
+
+Key rotation is a well-known paradigm. We'll update `README.md` of the `EncryptedSavedObjects` plugin and create a dedicated section in the public Kibana documentation.
+
+# Unresolved questions
+
+* Is it reasonable to have this feature in Basic?
+* Are there any other use-cases that are not covered by the proposal?
diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts
index eccc9d013176c..acb83962bd457 100644
--- a/src/core/server/http/base_path_proxy_server.ts
+++ b/src/core/server/http/base_path_proxy_server.ts
@@ -22,10 +22,9 @@ import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https';
import apm from 'elastic-apm-node';
import { ByteSizeValue } from '@kbn/config-schema';
-import { Server, Request, ResponseToolkit } from 'hapi';
+import { Server, Request } from 'hapi';
import HapiProxy from 'h2o2';
import { sampleSize } from 'lodash';
-import BrowserslistUserAgent from 'browserslist-useragent';
import * as Rx from 'rxjs';
import { take } from 'rxjs/operators';
@@ -41,34 +40,6 @@ export interface BasePathProxyServerOptions {
delayUntil: () => Rx.Observable;
}
-// Before we proxy request to a target port we may want to wait until some
-// condition is met (e.g. until target listener is ready).
-const checkForBrowserCompat = (log: Logger) => async (request: Request, h: ResponseToolkit) => {
- if (!request.headers['user-agent'] || process.env.BROWSERSLIST_ENV === 'production') {
- return h.continue;
- }
-
- const matches = BrowserslistUserAgent.matchesUA(request.headers['user-agent'], {
- env: 'dev',
- allowHigherVersions: true,
- ignoreMinor: true,
- ignorePath: true,
- });
-
- if (!matches) {
- log.warn(`
- Request with user-agent [${request.headers['user-agent']}]
- seems like it is coming from a browser that is not supported by the dev browserlist.
-
- Please run Kibana with the environment variable BROWSERSLIST_ENV=production to enable
- support for all production browsers (like IE).
-
- `);
- }
-
- return h.continue;
-};
-
export class BasePathProxyServer {
private server?: Server;
private httpsAgent?: HttpsAgent;
@@ -155,9 +126,6 @@ export class BasePathProxyServer {
},
method: 'GET',
path: '/',
- options: {
- pre: [checkForBrowserCompat(this.log)],
- },
});
this.server.route({
@@ -175,7 +143,6 @@ export class BasePathProxyServer {
method: '*',
options: {
pre: [
- checkForBrowserCompat(this.log),
// Before we proxy request to a target port we may want to wait until some
// condition is met (e.g. until target listener is ready).
async (request, responseToolkit) => {
@@ -210,7 +177,6 @@ export class BasePathProxyServer {
method: '*',
options: {
pre: [
- checkForBrowserCompat(this.log),
// Before we proxy request to a target port we may want to wait until some
// condition is met (e.g. until target listener is ready).
async (request, responseToolkit) => {
diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js
index 8b8fdcb7a76ac..abe04fb8bd7e3 100644
--- a/src/plugins/dashboard/public/application/legacy_app.js
+++ b/src/plugins/dashboard/public/application/legacy_app.js
@@ -30,6 +30,7 @@ import {
createKbnUrlStateStorage,
redirectWhenMissing,
SavedObjectNotFound,
+ withNotifyOnErrors,
} from '../../../kibana_utils/public';
import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing';
import { addHelpMenuToAppChrome } from './help_menu/help_menu_util';
@@ -65,6 +66,7 @@ export function initDashboardApp(app, deps) {
createKbnUrlStateStorage({
history,
useHash: deps.uiSettings.get('state:storeInSessionStorage'),
+ ...withNotifyOnErrors(deps.core.notifications.toasts),
})
);
diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js
index a6f591eebb52d..6223090aa9f97 100644
--- a/src/plugins/discover/public/application/angular/context.js
+++ b/src/plugins/discover/public/application/angular/context.js
@@ -83,6 +83,7 @@ function ContextAppRouteController($routeParams, $scope, $route) {
timeFieldName: indexPattern.timeFieldName,
storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'),
history: getServices().history(),
+ toasts: getServices().core.notifications.toasts,
});
this.state = { ...appState.getState() };
this.anchorId = $routeParams.id;
diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts
index 7a92a6ace125b..5b05d8729c41d 100644
--- a/src/plugins/discover/public/application/angular/context_state.ts
+++ b/src/plugins/discover/public/application/angular/context_state.ts
@@ -18,11 +18,13 @@
*/
import _ from 'lodash';
import { History } from 'history';
+import { NotificationsStart } from 'kibana/public';
import {
createStateContainer,
createKbnUrlStateStorage,
syncStates,
BaseStateContainer,
+ withNotifyOnErrors,
} from '../../../../kibana_utils/public';
import { esFilters, FilterManager, Filter, Query } from '../../../../data/public';
@@ -74,6 +76,13 @@ interface GetStateParams {
* History instance to use
*/
history: History;
+
+ /**
+ * Core's notifications.toasts service
+ * In case it is passed in,
+ * kbnUrlStateStorage will use it notifying about inner errors
+ */
+ toasts?: NotificationsStart['toasts'];
}
interface GetStateReturn {
@@ -123,10 +132,12 @@ export function getState({
timeFieldName,
storeInSessionStorage = false,
history,
+ toasts,
}: GetStateParams): GetStateReturn {
const stateStorage = createKbnUrlStateStorage({
useHash: storeInSessionStorage,
history,
+ ...(toasts && withNotifyOnErrors(toasts)),
});
const globalStateInitial = stateStorage.get(GLOBAL_STATE_URL_KEY) as GlobalState;
diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js
index 4a27f261a6220..22da3e877054a 100644
--- a/src/plugins/discover/public/application/angular/discover.js
+++ b/src/plugins/discover/public/application/angular/discover.js
@@ -220,6 +220,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise
defaultAppState: getStateDefaults(),
storeInSessionStorage: config.get('state:storeInSessionStorage'),
history,
+ toasts: core.notifications.toasts,
});
if (appStateContainer.getState().index !== $scope.indexPattern.id) {
//used index pattern is different than the given by url/state which is invalid
diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts
index 46500d9fdf85e..ff8fb9f80a723 100644
--- a/src/plugins/discover/public/application/angular/discover_state.ts
+++ b/src/plugins/discover/public/application/angular/discover_state.ts
@@ -18,12 +18,14 @@
*/
import { isEqual } from 'lodash';
import { History } from 'history';
+import { NotificationsStart } from 'kibana/public';
import {
createStateContainer,
createKbnUrlStateStorage,
syncState,
ReduxLikeStateContainer,
IKbnUrlStateStorage,
+ withNotifyOnErrors,
} from '../../../../kibana_utils/public';
import { esFilters, Filter, Query } from '../../../../data/public';
import { migrateLegacyQuery } from '../../../../kibana_legacy/public';
@@ -68,6 +70,13 @@ interface GetStateParams {
* Browser history
*/
history: History;
+
+ /**
+ * Core's notifications.toasts service
+ * In case it is passed in,
+ * kbnUrlStateStorage will use it notifying about inner errors
+ */
+ toasts?: NotificationsStart['toasts'];
}
export interface GetStateReturn {
@@ -122,10 +131,12 @@ export function getState({
defaultAppState = {},
storeInSessionStorage = false,
history,
+ toasts,
}: GetStateParams): GetStateReturn {
const stateStorage = createKbnUrlStateStorage({
useHash: storeInSessionStorage,
history,
+ ...(toasts && withNotifyOnErrors(toasts)),
});
const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState;
diff --git a/src/plugins/es_ui_shared/README.md b/src/plugins/es_ui_shared/README.md
new file mode 100644
index 0000000000000..5a9091e2dd1eb
--- /dev/null
+++ b/src/plugins/es_ui_shared/README.md
@@ -0,0 +1,32 @@
+## ES UI shared modules
+
+This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module.
+
+**Please note**: Modules in ES UI shared are intended for use by the ES UI Management Team (elastic/es-ui@) only. Please reach out to us if there is something you would like to contribute or use in these modules.
+
+## Files and folders overview
+
+- `./public` | `./server`. Folders for grouping server or public code according to the Kibana plugin pattern.
+- `./__packages_do_not_import__` is where actual functionality is kept. This enables modules more control over what functionality is directly exported and prevents parts of modules to be depended on externally in unintended ways.
+- `./public/index.ts` | `./server/index.ts` These files export modules (simple JavaScript objects). For example, `Monaco` is the name of a module. In this way, modules namespace all of their exports and do not have to be concerned about name collisions from other modules.
+
+## Conventions for adding code
+
+When adding new functionality, look at the folders in `./__packages_do_not_import__` and consider whether your functionality falls into any of those modules.
+
+If it does not, you should create a module and expose it to public or server code (or both) following the conventions described above.
+
+### Example
+
+If I wanted to add functionality for calculating a Fibonacci sequence browser-side one would do the following:
+
+1. Create a folder `./__packages_do_not_import__/math`. The name of the folder should be a snake_case version of the module name. In this case `Math` -> `math`. Another case, `IndexManagement` -> `index_management`.
+2. Write your function in `./__packages_do_not_import__/math/calculate_fibonacci.ts`, adding any relevant tests in the same folder.
+3. Export functionality intended _for consumers_ from `./__packages_do_not_import__/math/index.ts`.
+4. Create a folder `./public/math`.
+5. Export all functionality from `./__packages_do_not_import__/math` in `./public/math/index.ts`.
+6. In `./public/index.ts` import `./public/math` using `import * as Math from './public/math;`. The name (`Math`) given here is really important and will be what consumers depend on.
+7. Add the `Math` module to the list of exported modules in `./public/index.ts`, e.g. `export { <...other modules>, Math }`
+8. Use `Math` in your public side code elsewhere!
+
+This example assumes no other appropriate home for such a function exists.
diff --git a/src/plugins/kibana_utils/docs/state_sync/README.md b/src/plugins/kibana_utils/docs/state_sync/README.md
index acfe6dcf76fe9..c84bf7f236330 100644
--- a/src/plugins/kibana_utils/docs/state_sync/README.md
+++ b/src/plugins/kibana_utils/docs/state_sync/README.md
@@ -58,3 +58,4 @@ To run them, start kibana with `--run-examples` flag.
- [On-the-fly state migrations](./on_fly_state_migrations.md).
- [syncStates helper](./sync_states.md).
- [Helpers for Data plugin (syncing TimeRange, RefreshInterval and Filters)](./data_plugin_helpers.md).
+- [Error handling](./error_handling.md)
diff --git a/src/plugins/kibana_utils/docs/state_sync/error_handling.md b/src/plugins/kibana_utils/docs/state_sync/error_handling.md
new file mode 100644
index 0000000000000..b12e1040af260
--- /dev/null
+++ b/src/plugins/kibana_utils/docs/state_sync/error_handling.md
@@ -0,0 +1,6 @@
+# Error handling
+
+State syncing util doesn't have specific api for handling errors.
+It expects that errors are handled on storage level.
+
+see [KbnUrlStateStorage](./storages/kbn_url_storage.md#) error handling section for details.
diff --git a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md
index 3a31f5a326edb..ec27895eed666 100644
--- a/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md
+++ b/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md
@@ -65,7 +65,7 @@ To prevent bugs caused by missing history updates, make sure your app uses one i
For example, if you use `react-router`:
```tsx
-const App = props => {
+const App = (props) => {
useEffect(() => {
const stateStorage = createKbnUrlStateStorage({
useHash: props.uiSettings.get('state:storeInSessionStorage'),
@@ -160,3 +160,58 @@ const { start, stop } = syncStates([
;
```
+
+### Error handling
+
+Errors could occur both during `kbnUrlStateStorage.get()` and `kbnUrlStateStorage.set()`
+
+#### Handling kbnUrlStateStorage.get() errors
+
+Possible error scenarios during `kbnUrlStateStorage.get()`:
+
+1. Rison in URL is malformed. Parsing exception.
+2. useHash is enabled and current hash is missing in `sessionStorage`
+
+In all the cases error is handled internally and `kbnUrlStateStorage.get()` returns `null`, just like if there is no state in the URL anymore
+
+You can pass callback to get notified about errors. Use it, for example, for notifying users
+
+```ts
+const kbnUrlStateStorage = createKbnUrlStateStorage({
+ history,
+ onGetError: (error) => {
+ alert(error.message);
+ },
+});
+```
+
+#### Handling kbnUrlStateStorage.set() errors
+
+Possible errors during `kbnUrlStateStorage.set()`:
+
+1. `useHash` is enabled and can't store state in `sessionStorage` (overflow or no access)
+
+In all the cases error is handled internally and URL update is skipped
+
+You can pass callback to get notified about errors. Use it, for example, for notifying users:
+
+```ts
+const kbnUrlStateStorage = createKbnUrlStateStorage({
+ history,
+ onSetError: (error) => {
+ alert(error.message);
+ },
+});
+```
+
+#### Helper to integrate with core.notifications.toasts
+
+The most common scenario is to notify users about issues with state syncing using toast service from core
+There is a convenient helper for this:
+
+```ts
+const kbnUrlStateStorage = createKbnUrlStateStorage({
+ history,
+ ...withNotifyOnErrors(core.notifications.toasts),
+});
+```
diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts
index e2d6ae647abb1..d1c9eec0e9906 100644
--- a/src/plugins/kibana_utils/public/index.ts
+++ b/src/plugins/kibana_utils/public/index.ts
@@ -57,6 +57,7 @@ export {
getStateFromKbnUrl,
getStatesFromKbnUrl,
setStateToKbnUrl,
+ withNotifyOnErrors,
} from './state_management/url';
export {
syncState,
diff --git a/src/plugins/kibana_utils/public/state_management/url/errors.ts b/src/plugins/kibana_utils/public/state_management/url/errors.ts
new file mode 100644
index 0000000000000..b8b6523e8070c
--- /dev/null
+++ b/src/plugins/kibana_utils/public/state_management/url/errors.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { NotificationsStart } from 'kibana/public';
+
+export const restoreUrlErrorTitle = i18n.translate(
+ 'kibana_utils.stateManagement.url.restoreUrlErrorTitle',
+ {
+ defaultMessage: `Error restoring state from URL`,
+ }
+);
+
+export const saveStateInUrlErrorTitle = i18n.translate(
+ 'kibana_utils.stateManagement.url.saveStateInUrlErrorTitle',
+ {
+ defaultMessage: `Error saving state in URL`,
+ }
+);
+
+/**
+ * Helper for configuring {@link IKbnUrlStateStorage} to notify about inner errors
+ *
+ * @example
+ * ```ts
+ * const kbnUrlStateStorage = createKbnUrlStateStorage({
+ * history,
+ * ...withNotifyOnErrors(core.notifications.toast))
+ * }
+ * ```
+ * @param toast - toastApi from core.notifications.toasts
+ */
+export const withNotifyOnErrors = (toasts: NotificationsStart['toasts']) => {
+ return {
+ onGetError: (error: Error) => {
+ toasts.addError(error, {
+ title: restoreUrlErrorTitle,
+ });
+ },
+ onSetError: (error: Error) => {
+ toasts.addError(error, {
+ title: saveStateInUrlErrorTitle,
+ });
+ },
+ };
+};
diff --git a/src/plugins/kibana_utils/public/state_management/url/index.ts b/src/plugins/kibana_utils/public/state_management/url/index.ts
index e28d183c6560a..66fecd723e3ba 100644
--- a/src/plugins/kibana_utils/public/state_management/url/index.ts
+++ b/src/plugins/kibana_utils/public/state_management/url/index.ts
@@ -27,3 +27,4 @@ export {
} from './kbn_url_storage';
export { createKbnUrlTracker } from './kbn_url_tracker';
export { createUrlTracker } from './url_tracker';
+export { withNotifyOnErrors, saveStateInUrlErrorTitle, restoreUrlErrorTitle } from './errors';
diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts
index d9149095a2fa2..fefd5f668c6b3 100644
--- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts
+++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts
@@ -103,7 +103,7 @@ export function setStateToKbnUrl(
export interface IKbnUrlControls {
/**
* Listen for url changes
- * @param cb - get's called when url has been changed
+ * @param cb - called when url has been changed
*/
listen: (cb: () => void) => () => void;
@@ -142,12 +142,12 @@ export interface IKbnUrlControls {
*/
cancel: () => void;
}
-export type UrlUpdaterFnType = (currentUrl: string) => string;
+export type UrlUpdaterFnType = (currentUrl: string) => string | undefined;
export const createKbnUrlControls = (
history: History = createBrowserHistory()
): IKbnUrlControls => {
- const updateQueue: Array<(currentUrl: string) => string> = [];
+ const updateQueue: UrlUpdaterFnType[] = [];
// if we should replace or push with next async update,
// if any call in a queue asked to push, then we should push
@@ -188,7 +188,7 @@ export const createKbnUrlControls = (
function getPendingUrl() {
if (updateQueue.length === 0) return undefined;
const resultUrl = updateQueue.reduce(
- (url, nextUpdate) => nextUpdate(url),
+ (url, nextUpdate) => nextUpdate(url) ?? url,
getCurrentUrl(history)
);
@@ -201,7 +201,7 @@ export const createKbnUrlControls = (
cb();
}),
update: (newUrl: string, replace = false) => updateUrl(newUrl, replace),
- updateAsync: (updater: (currentUrl: string) => string, replace = false) => {
+ updateAsync: (updater: UrlUpdaterFnType, replace = false) => {
updateQueue.push(updater);
if (shouldReplace) {
shouldReplace = replace;
diff --git a/src/plugins/kibana_utils/public/state_sync/public.api.md b/src/plugins/kibana_utils/public/state_sync/public.api.md
index ae8c0e8e401b8..a4dfea82cdb59 100644
--- a/src/plugins/kibana_utils/public/state_sync/public.api.md
+++ b/src/plugins/kibana_utils/public/state_sync/public.api.md
@@ -8,9 +8,11 @@ import { History } from 'history';
import { Observable } from 'rxjs';
// @public
-export const createKbnUrlStateStorage: ({ useHash, history }?: {
+export const createKbnUrlStateStorage: ({ useHash, history, onGetError, onSetError, }?: {
useHash: boolean;
history?: History | undefined;
+ onGetError?: ((error: Error) => void) | undefined;
+ onSetError?: ((error: Error) => void) | undefined;
}) => IKbnUrlStateStorage;
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Storage"
diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts
index cc708d14ea8b5..e222af91d7729 100644
--- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts
+++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts
@@ -16,12 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-import '../../storage/hashed_item_store/mock';
+import { mockStorage } from '../../storage/hashed_item_store/mock';
import { createKbnUrlStateStorage, IKbnUrlStateStorage } from './create_kbn_url_state_storage';
import { History, createBrowserHistory } from 'history';
import { takeUntil, toArray } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { ScopedHistory } from '../../../../../core/public';
+import { withNotifyOnErrors } from '../../state_management/url';
+import { coreMock } from '../../../../../core/public/mocks';
describe('KbnUrlStateStorage', () => {
describe('useHash: false', () => {
@@ -93,6 +95,37 @@ describe('KbnUrlStateStorage', () => {
expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]);
});
+
+ it("shouldn't throw in case of parsing error", async () => {
+ const key = '_s';
+ history.replace(`/#?${key}=(ok:2,test:`); // malformed rison
+ expect(() => urlStateStorage.get(key)).not.toThrow();
+ expect(urlStateStorage.get(key)).toBeNull();
+ });
+
+ it('should notify about errors', () => {
+ const cb = jest.fn();
+ urlStateStorage = createKbnUrlStateStorage({ useHash: false, history, onGetError: cb });
+ const key = '_s';
+ history.replace(`/#?${key}=(ok:2,test:`); // malformed rison
+ expect(() => urlStateStorage.get(key)).not.toThrow();
+ expect(cb).toBeCalledWith(expect.any(Error));
+ });
+
+ describe('withNotifyOnErrors integration', () => {
+ test('toast is shown', () => {
+ const toasts = coreMock.createStart().notifications.toasts;
+ urlStateStorage = createKbnUrlStateStorage({
+ useHash: true,
+ history,
+ ...withNotifyOnErrors(toasts),
+ });
+ const key = '_s';
+ history.replace(`/#?${key}=(ok:2,test:`); // malformed rison
+ expect(() => urlStateStorage.get(key)).not.toThrow();
+ expect(toasts.addError).toBeCalled();
+ });
+ });
});
describe('useHash: true', () => {
@@ -128,6 +161,44 @@ describe('KbnUrlStateStorage', () => {
expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]);
});
+
+ describe('hashStorage overflow exception', () => {
+ let oldLimit: number;
+ beforeAll(() => {
+ oldLimit = mockStorage.getStubbedSizeLimit();
+ mockStorage.clear();
+ mockStorage.setStubbedSizeLimit(0);
+ });
+ afterAll(() => {
+ mockStorage.setStubbedSizeLimit(oldLimit);
+ });
+
+ it("shouldn't throw in case of error", async () => {
+ expect(() => urlStateStorage.set('_s', { test: 'test' })).not.toThrow();
+ await expect(urlStateStorage.set('_s', { test: 'test' })).resolves; // not rejects
+ expect(getCurrentUrl()).toBe('/'); // url wasn't updated with hash
+ });
+
+ it('should notify about errors', async () => {
+ const cb = jest.fn();
+ urlStateStorage = createKbnUrlStateStorage({ useHash: true, history, onSetError: cb });
+ await expect(urlStateStorage.set('_s', { test: 'test' })).resolves; // not rejects
+ expect(cb).toBeCalledWith(expect.any(Error));
+ });
+
+ describe('withNotifyOnErrors integration', () => {
+ test('toast is shown', async () => {
+ const toasts = coreMock.createStart().notifications.toasts;
+ urlStateStorage = createKbnUrlStateStorage({
+ useHash: true,
+ history,
+ ...withNotifyOnErrors(toasts),
+ });
+ await expect(urlStateStorage.set('_s', { test: 'test' })).resolves; // not rejects
+ expect(toasts.addError).toBeCalled();
+ });
+ });
+ });
});
describe('ScopedHistory integration', () => {
diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts
index 0c74e1eb9f421..460720b98e30f 100644
--- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts
+++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts
@@ -17,8 +17,8 @@
* under the License.
*/
-import { Observable } from 'rxjs';
-import { map, share } from 'rxjs/operators';
+import { Observable, of } from 'rxjs';
+import { catchError, map, share } from 'rxjs/operators';
import { History } from 'history';
import { IStateStorage } from './types';
import {
@@ -68,7 +68,19 @@ export interface IKbnUrlStateStorage extends IStateStorage {
* @public
*/
export const createKbnUrlStateStorage = (
- { useHash = false, history }: { useHash: boolean; history?: History } = { useHash: false }
+ {
+ useHash = false,
+ history,
+ onGetError,
+ onSetError,
+ }: {
+ useHash: boolean;
+ history?: History;
+ onGetError?: (error: Error) => void;
+ onSetError?: (error: Error) => void;
+ } = {
+ useHash: false,
+ }
): IKbnUrlStateStorage => {
const url = createKbnUrlControls(history);
return {
@@ -78,15 +90,23 @@ export const createKbnUrlStateStorage = (
{ replace = false }: { replace: boolean } = { replace: false }
) => {
// syncState() utils doesn't wait for this promise
- return url.updateAsync(
- (currentUrl) => setStateToKbnUrl(key, state, { useHash }, currentUrl),
- replace
- );
+ return url.updateAsync((currentUrl) => {
+ try {
+ return setStateToKbnUrl(key, state, { useHash }, currentUrl);
+ } catch (error) {
+ if (onSetError) onSetError(error);
+ }
+ }, replace);
},
get: (key) => {
// if there is a pending url update, then state will be extracted from that pending url,
// otherwise current url will be used to retrieve state from
- return getStateFromKbnUrl(key, url.getPendingUrl());
+ try {
+ return getStateFromKbnUrl(key, url.getPendingUrl());
+ } catch (e) {
+ if (onGetError) onGetError(e);
+ return null;
+ }
},
change$: (key: string) =>
new Observable((observer) => {
@@ -99,6 +119,10 @@ export const createKbnUrlStateStorage = (
};
}).pipe(
map(() => getStateFromKbnUrl(key)),
+ catchError((error) => {
+ if (onGetError) onGetError(error);
+ return of(null);
+ }),
share()
),
flush: ({ replace = false }: { replace?: boolean } = {}) => {
diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js
index 0294e71084f98..614a7539de44c 100644
--- a/src/plugins/timelion/public/app.js
+++ b/src/plugins/timelion/public/app.js
@@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n';
import { createHashHistory } from 'history';
-import { createKbnUrlStateStorage } from '../../kibana_utils/public';
+import { createKbnUrlStateStorage, withNotifyOnErrors } from '../../kibana_utils/public';
import { syncQueryStateWithUrl } from '../../data/public';
import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs';
@@ -63,6 +63,7 @@ export function initTimelionApp(app, deps) {
createKbnUrlStateStorage({
history,
useHash: deps.core.uiSettings.get('state:storeInSessionStorage'),
+ ...withNotifyOnErrors(deps.core.notifications.toasts),
})
);
app.config(watchMultiDecorator);
diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts
index a462e488c6732..c1730e6a15435 100644
--- a/src/plugins/vis_type_timeseries/common/vis_schema.ts
+++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts
@@ -119,6 +119,10 @@ export const metricsItems = schema.object({
type: stringRequired,
value: stringOptionalNullable,
values: schema.maybe(schema.nullable(schema.arrayOf(schema.nullable(schema.string())))),
+ size: stringOptionalNullable,
+ agg_with: stringOptionalNullable,
+ order: stringOptionalNullable,
+ order_by: stringOptionalNullable,
});
const splitFiltersItems = schema.object({
diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
index 392168a530087..da9ba66a914dd 100644
--- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
+++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
@@ -114,7 +114,7 @@ export const getTopNavConfig = (
application.navigateToApp(originatingApp);
}
} else {
- if (setOriginatingApp && originatingApp && savedVis.copyOnSave) {
+ if (setOriginatingApp && originatingApp && newlyCreated) {
setOriginatingApp(undefined);
}
chrome.docTitle.change(savedVis.lastSavedTitle);
diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts
index fd9a67599414f..3299319e613a0 100644
--- a/src/plugins/visualize/public/plugin.ts
+++ b/src/plugins/visualize/public/plugin.ts
@@ -31,7 +31,12 @@ import {
ScopedHistory,
} from 'kibana/public';
-import { Storage, createKbnUrlTracker, createKbnUrlStateStorage } from '../../kibana_utils/public';
+import {
+ Storage,
+ createKbnUrlTracker,
+ createKbnUrlStateStorage,
+ withNotifyOnErrors,
+} from '../../kibana_utils/public';
import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public';
import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public';
import { SharePluginStart } from '../../share/public';
@@ -150,6 +155,7 @@ export class VisualizePlugin
kbnUrlStateStorage: createKbnUrlStateStorage({
history,
useHash: coreStart.uiSettings.get('state:storeInSessionStorage'),
+ ...withNotifyOnErrors(coreStart.notifications.toasts),
}),
kibanaLegacy: pluginsStart.kibanaLegacy,
pluginInitializerContext: this.initializerContext,
diff --git a/test/functional/apps/dashboard/edit_embeddable_redirects.js b/test/functional/apps/dashboard/edit_embeddable_redirects.js
index 6d3d43890a962..fcc504ea24f31 100644
--- a/test/functional/apps/dashboard/edit_embeddable_redirects.js
+++ b/test/functional/apps/dashboard/edit_embeddable_redirects.js
@@ -21,8 +21,10 @@ import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']);
const esArchiver = getService('esArchiver');
+ const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
const dashboardPanelActions = getService('dashboardPanelActions');
+ const dashboardVisualizations = getService('dashboardVisualizations');
describe('edit embeddable redirects', () => {
before(async () => {
@@ -81,6 +83,23 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
await dashboardPanelActions.openContextMenu();
await dashboardPanelActions.clickEdit();
+ await PageObjects.visualize.linkedToOriginatingApp();
+ await PageObjects.visualize.saveVisualizationExpectSuccess(newTitle, {
+ saveAsNew: true,
+ redirectToOrigin: false,
+ });
+ await PageObjects.visualize.notLinkedToOriginatingApp();
+ await PageObjects.common.navigateToApp('dashboard');
+ });
+
+ it('loses originatingApp connection after first save when redirectToOrigin is false', async () => {
+ const newTitle = 'test create panel originatingApp';
+ await PageObjects.dashboard.loadSavedDashboard('few panels');
+ await PageObjects.dashboard.switchToEditMode();
+ await testSubjects.exists('dashboardAddNewPanelButton');
+ await testSubjects.click('dashboardAddNewPanelButton');
+ await dashboardVisualizations.ensureNewVisualizationDialogIsShowing();
+ await PageObjects.visualize.clickMarkdownWidget();
await PageObjects.visualize.saveVisualizationExpectSuccess(newTitle, {
saveAsNew: true,
redirectToOrigin: false,
diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js
index 5c6a70450a0aa..94409a94e9257 100644
--- a/test/functional/apps/discover/_shared_links.js
+++ b/test/functional/apps/discover/_shared_links.js
@@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'discover', 'share', 'timePicker']);
const browser = getService('browser');
+ const toasts = getService('toasts');
describe('shared links', function describeIndexTests() {
let baseUrl;
@@ -132,28 +133,47 @@ export default function ({ getService, getPageObjects }) {
await teardown();
});
- describe('permalink', function () {
- it('should allow for copying the snapshot URL as a short URL and should open it', async function () {
- const re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$');
- await PageObjects.share.checkShortenUrl();
- let actualUrl;
- await retry.try(async () => {
- actualUrl = await PageObjects.share.getSharedUrl();
- expect(actualUrl).to.match(re);
- });
+ it('should allow for copying the snapshot URL as a short URL and should open it', async function () {
+ const re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$');
+ await PageObjects.share.checkShortenUrl();
+ let actualUrl;
+ await retry.try(async () => {
+ actualUrl = await PageObjects.share.getSharedUrl();
+ expect(actualUrl).to.match(re);
+ });
- const actualTime = await PageObjects.timePicker.getTimeConfig();
-
- await browser.clearSessionStorage();
- await browser.get(actualUrl, false);
- await retry.waitFor('shortUrl resolves and opens', async () => {
- const resolvedUrl = await browser.getCurrentUrl();
- expect(resolvedUrl).to.match(/discover/);
- const resolvedTime = await PageObjects.timePicker.getTimeConfig();
- expect(resolvedTime.start).to.equal(actualTime.start);
- expect(resolvedTime.end).to.equal(actualTime.end);
- return true;
- });
+ const actualTime = await PageObjects.timePicker.getTimeConfig();
+
+ await browser.clearSessionStorage();
+ await browser.get(actualUrl, false);
+ await retry.waitFor('shortUrl resolves and opens', async () => {
+ const resolvedUrl = await browser.getCurrentUrl();
+ expect(resolvedUrl).to.match(/discover/);
+ const resolvedTime = await PageObjects.timePicker.getTimeConfig();
+ expect(resolvedTime.start).to.equal(actualTime.start);
+ expect(resolvedTime.end).to.equal(actualTime.end);
+ return true;
+ });
+ });
+
+ it("sharing hashed url shouldn't crash the app", async () => {
+ const currentUrl = await browser.getCurrentUrl();
+ const timeBeforeReload = await PageObjects.timePicker.getTimeConfig();
+ await browser.clearSessionStorage();
+ await browser.get(currentUrl, false);
+ await retry.waitFor('discover to open', async () => {
+ const resolvedUrl = await browser.getCurrentUrl();
+ expect(resolvedUrl).to.match(/discover/);
+ const { message } = await toasts.getErrorToast();
+ expect(message).to.contain(
+ 'Unable to completely restore the URL, be sure to use the share functionality.'
+ );
+ await toasts.dismissAllToasts();
+ const timeAfterReload = await PageObjects.timePicker.getTimeConfig();
+ expect(timeBeforeReload.start).not.to.be(timeAfterReload.start);
+ expect(timeBeforeReload.end).not.to.be(timeAfterReload.end);
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ return true;
});
});
});
diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts
index 92f1f726fa039..a70e4ba464ae8 100644
--- a/test/functional/services/toasts.ts
+++ b/test/functional/services/toasts.ts
@@ -53,6 +53,16 @@ export function ToastsProvider({ getService }: FtrProviderContext) {
await dismissButton.click();
}
+ public async dismissAllToasts() {
+ const list = await this.getGlobalToastList();
+ const toasts = await list.findAllByCssSelector(`.euiToast`);
+ for (const toast of toasts) {
+ await toast.moveMouseTo();
+ const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast);
+ await dismissButton.click();
+ }
+ }
+
private async getToastElement(index: number) {
const list = await this.getGlobalToastList();
return await list.findByCssSelector(`.euiToast:nth-child(${index})`);
diff --git a/x-pack/package.json b/x-pack/package.json
index dcba01a771fd5..8fbb94c97c143 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -199,9 +199,9 @@
"yargs": "4.8.1"
},
"dependencies": {
- "@babel/core": "^7.10.2",
- "@babel/register": "^7.10.1",
- "@babel/runtime": "^7.10.2",
+ "@babel/core": "^7.11.1",
+ "@babel/register": "^7.10.5",
+ "@babel/runtime": "^7.11.2",
"@elastic/apm-rum-react": "^1.1.2",
"@elastic/datemath": "5.0.3",
"@elastic/ems-client": "7.9.3",
diff --git a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx b/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx
index b0a8d1e990e75..1bb58919b7fa6 100644
--- a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx
+++ b/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx
@@ -6,8 +6,8 @@
import React from 'react';
import { mount } from 'enzyme';
-// @ts-expect-error untyped local
-import { ExportApp } from '../export_app';
+import { ExportApp } from '../export_app.component';
+import { CanvasWorkpad } from '../../../../../types';
jest.mock('style-it', () => ({
it: (css: string, Component: any) => Component,
@@ -23,7 +23,7 @@ jest.mock('../../../../components/link', () => ({
describe('', () => {
test('renders as expected', () => {
- const sampleWorkpad = {
+ const sampleWorkpad = ({
id: 'my-workpad-abcd',
css: '',
pages: [
@@ -34,7 +34,7 @@ describe('', () => {
elements: [3, 4, 5, 6],
},
],
- };
+ } as any) as CanvasWorkpad;
const page1 = mount(
{}} />
diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx b/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx
new file mode 100644
index 0000000000000..03121e749d0dc
--- /dev/null
+++ b/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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, { FC, useEffect } from 'react';
+import PropTypes from 'prop-types';
+// @ts-expect-error untyped library
+import Style from 'style-it';
+// @ts-expect-error untyped local
+import { WorkpadPage } from '../../../components/workpad_page';
+import { Link } from '../../../components/link';
+import { CanvasWorkpad } from '../../../../types';
+
+interface Props {
+ workpad: CanvasWorkpad;
+ selectedPageIndex: number;
+ initializeWorkpad: () => void;
+}
+
+export const ExportApp: FC = ({ workpad, selectedPageIndex, initializeWorkpad }) => {
+ const { id, pages, height, width } = workpad;
+ const activePage = pages[selectedPageIndex];
+ const pageElementCount = activePage.elements.length;
+
+ useEffect(() => initializeWorkpad());
+
+ return (
+
+
+
+
+ Edit Workpad
+
+
+ {Style.it(
+ workpad.css,
+
+ {}}
+ unregisterLayout={() => {}}
+ />
+
+ )}
+
+
+ );
+};
+
+ExportApp.propTypes = {
+ workpad: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ pages: PropTypes.array.isRequired,
+ }).isRequired,
+ selectedPageIndex: PropTypes.number.isRequired,
+ initializeWorkpad: PropTypes.func.isRequired,
+};
diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.js b/x-pack/plugins/canvas/public/apps/export/export/export_app.js
deleted file mode 100644
index 1d02d85cae0b3..0000000000000
--- a/x-pack/plugins/canvas/public/apps/export/export/export_app.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import Style from 'style-it';
-import { WorkpadPage } from '../../../components/workpad_page';
-import { Link } from '../../../components/link';
-
-export class ExportApp extends React.PureComponent {
- static propTypes = {
- workpad: PropTypes.shape({
- id: PropTypes.string.isRequired,
- pages: PropTypes.array.isRequired,
- }).isRequired,
- selectedPageIndex: PropTypes.number.isRequired,
- initializeWorkpad: PropTypes.func.isRequired,
- };
-
- componentDidMount() {
- this.props.initializeWorkpad();
- }
-
- render() {
- const { workpad, selectedPageIndex } = this.props;
- const { pages, height, width } = workpad;
- const activePage = pages[selectedPageIndex];
- const pageElementCount = activePage.elements.length;
-
- return (
-
-
-
-
- Edit Workpad
-
-
- {Style.it(
- workpad.css,
-
- {}}
- unregisterLayout={() => {}}
- />
-
- )}
-
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.ts b/x-pack/plugins/canvas/public/apps/export/export/export_app.ts
new file mode 100644
index 0000000000000..b47d1950ec2b7
--- /dev/null
+++ b/x-pack/plugins/canvas/public/apps/export/export/export_app.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { connect } from 'react-redux';
+import { initializeWorkpad } from '../../../state/actions/workpad';
+import { getWorkpad, getSelectedPageIndex } from '../../../state/selectors/workpad';
+import { ExportApp as Component } from './export_app.component';
+import { State } from '../../../../types';
+
+export const ExportApp = connect(
+ (state: State) => ({
+ workpad: getWorkpad(state),
+ selectedPageIndex: getSelectedPageIndex(state),
+ }),
+ (dispatch) => ({
+ initializeWorkpad: () => dispatch(initializeWorkpad()),
+ })
+)(Component);
diff --git a/x-pack/plugins/canvas/public/apps/export/export/index.js b/x-pack/plugins/canvas/public/apps/export/export/index.js
deleted file mode 100644
index 95c46d9e1c8ae..0000000000000
--- a/x-pack/plugins/canvas/public/apps/export/export/index.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { connect } from 'react-redux';
-import { compose, branch, renderComponent } from 'recompose';
-import { initializeWorkpad } from '../../../state/actions/workpad';
-import { getWorkpad, getSelectedPageIndex } from '../../../state/selectors/workpad';
-import { LoadWorkpad } from './load_workpad';
-import { ExportApp as Component } from './export_app';
-
-const mapStateToProps = (state) => ({
- workpad: getWorkpad(state),
- selectedPageIndex: getSelectedPageIndex(state),
-});
-
-const mapDispatchToProps = (dispatch) => ({
- initializeWorkpad() {
- dispatch(initializeWorkpad());
- },
-});
-
-const branches = [branch(({ workpad }) => workpad == null, renderComponent(LoadWorkpad))];
-
-export const ExportApp = compose(
- connect(mapStateToProps, mapDispatchToProps),
- ...branches
-)(Component);
diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/load_workpad.js b/x-pack/plugins/canvas/public/apps/export/export/index.ts
similarity index 67%
rename from x-pack/plugins/canvas/public/apps/workpad/workpad_app/load_workpad.js
rename to x-pack/plugins/canvas/public/apps/export/export/index.ts
index 388bf00723f82..81939d550a7ab 100644
--- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/load_workpad.js
+++ b/x-pack/plugins/canvas/public/apps/export/export/index.ts
@@ -4,6 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
-
-export const LoadWorkpad = () =>
Load a workpad...
;
+export { ExportApp } from './export_app';
+export { ExportApp as ExportAppComponent } from './export_app.component';
diff --git a/x-pack/plugins/canvas/public/apps/export/index.js b/x-pack/plugins/canvas/public/apps/export/index.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/apps/export/index.js
rename to x-pack/plugins/canvas/public/apps/export/index.ts
diff --git a/x-pack/plugins/canvas/public/apps/export/routes.js b/x-pack/plugins/canvas/public/apps/export/routes.ts
similarity index 79%
rename from x-pack/plugins/canvas/public/apps/export/routes.js
rename to x-pack/plugins/canvas/public/apps/export/routes.ts
index 33e375115aa19..0b4f74149fb4f 100644
--- a/x-pack/plugins/canvas/public/apps/export/routes.js
+++ b/x-pack/plugins/canvas/public/apps/export/routes.ts
@@ -4,10 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { Dispatch } from 'redux';
+// @ts-expect-error Untyped local
import * as workpadService from '../../lib/workpad_service';
import { setWorkpad } from '../../state/actions/workpad';
+// @ts-expect-error Untyped local
import { fetchAllRenderables } from '../../state/actions/elements';
+// @ts-expect-error Untyped local
import { setPage } from '../../state/actions/pages';
+// @ts-expect-error Untyped local
import { setAssets } from '../../state/actions/assets';
import { ExportApp } from './export';
@@ -18,7 +23,13 @@ export const routes = [
{
name: 'exportWorkpad',
path: '/pdf/:id/page/:page',
- action: (dispatch) => async ({ params, router }) => {
+ action: (dispatch: Dispatch) => async ({
+ params,
+ // @ts-expect-error Fix when Router is typed.
+ router,
+ }: {
+ params: { id: string; page: string };
+ }) => {
// load workpad if given a new id via url param
const fetchedWorkpad = await workpadService.get(params.id);
const pageNumber = parseInt(params.page, 10);
diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.js b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx
similarity index 79%
rename from x-pack/plugins/canvas/public/apps/home/home_app/home_app.js
rename to x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx
index bfa4abbf7c56d..3c2e989cc8e51 100644
--- a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.js
+++ b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx
@@ -4,12 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { FC } from 'react';
import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui';
+// @ts-expect-error untyped local
import { WorkpadManager } from '../../../components/workpad_manager';
+// @ts-expect-error untyped local
import { setDocTitle } from '../../../lib/doc_title';
-export const HomeApp = ({ onLoad = () => {} }) => {
+interface Props {
+ onLoad: () => void;
+}
+
+export const HomeApp: FC = ({ onLoad = () => {} }) => {
onLoad();
setDocTitle('Canvas');
return (
diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/index.js b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts
similarity index 69%
rename from x-pack/plugins/canvas/public/apps/home/home_app/index.js
rename to x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts
index f78ee1f8a18af..ff9d1c1cc63ac 100644
--- a/x-pack/plugins/canvas/public/apps/home/home_app/index.js
+++ b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts
@@ -6,12 +6,10 @@
import { connect } from 'react-redux';
import { resetWorkpad } from '../../../state/actions/workpad';
-import { HomeApp as Component } from './home_app';
+import { HomeApp as Component } from './home_app.component';
-const mapDispatchToProps = (dispatch) => ({
+export const HomeApp = connect(null, (dispatch) => ({
onLoad() {
dispatch(resetWorkpad());
},
-});
-
-export const HomeApp = connect(null, mapDispatchToProps)(Component);
+}))(Component);
diff --git a/x-pack/plugins/canvas/public/apps/export/export/load_workpad.js b/x-pack/plugins/canvas/public/apps/home/home_app/index.ts
similarity index 69%
rename from x-pack/plugins/canvas/public/apps/export/export/load_workpad.js
rename to x-pack/plugins/canvas/public/apps/home/home_app/index.ts
index 388bf00723f82..8ea92312e3e50 100644
--- a/x-pack/plugins/canvas/public/apps/export/export/load_workpad.js
+++ b/x-pack/plugins/canvas/public/apps/home/home_app/index.ts
@@ -4,6 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
-
-export const LoadWorkpad = () =>
Load a workpad...
;
+export { HomeApp } from './home_app';
+export { HomeApp as HomeAppComponent } from './home_app.component';
diff --git a/x-pack/plugins/canvas/public/apps/home/index.js b/x-pack/plugins/canvas/public/apps/home/index.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/apps/home/index.js
rename to x-pack/plugins/canvas/public/apps/home/index.ts
diff --git a/x-pack/plugins/canvas/public/apps/home/routes.js b/x-pack/plugins/canvas/public/apps/home/routes.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/apps/home/routes.js
rename to x-pack/plugins/canvas/public/apps/home/routes.ts
diff --git a/x-pack/plugins/canvas/public/apps/index.js b/x-pack/plugins/canvas/public/apps/index.ts
similarity index 88%
rename from x-pack/plugins/canvas/public/apps/index.js
rename to x-pack/plugins/canvas/public/apps/index.ts
index c014349ca18da..8b3d378e23f80 100644
--- a/x-pack/plugins/canvas/public/apps/index.js
+++ b/x-pack/plugins/canvas/public/apps/index.ts
@@ -8,6 +8,7 @@ import * as home from './home';
import * as workpad from './workpad';
import * as exp from './export';
+// @ts-expect-error Router and routes are not yet strongly typed
export const routes = [].concat(workpad.routes, home.routes, exp.routes);
export const apps = [workpad.WorkpadApp, home.HomeApp, exp.ExportApp];
diff --git a/x-pack/plugins/canvas/public/apps/workpad/index.js b/x-pack/plugins/canvas/public/apps/workpad/index.ts
similarity index 100%
rename from x-pack/plugins/canvas/public/apps/workpad/index.js
rename to x-pack/plugins/canvas/public/apps/workpad/index.ts
diff --git a/x-pack/plugins/canvas/public/apps/workpad/routes.js b/x-pack/plugins/canvas/public/apps/workpad/routes.ts
similarity index 82%
rename from x-pack/plugins/canvas/public/apps/workpad/routes.js
rename to x-pack/plugins/canvas/public/apps/workpad/routes.ts
index a330020b741ac..d83f85f717305 100644
--- a/x-pack/plugins/canvas/public/apps/workpad/routes.js
+++ b/x-pack/plugins/canvas/public/apps/workpad/routes.ts
@@ -4,17 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ErrorStrings } from '../../../i18n';
+import { Dispatch } from 'redux';
+// @ts-expect-error
import * as workpadService from '../../lib/workpad_service';
import { notifyService } from '../../services';
import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs';
+// @ts-expect-error
import { getDefaultWorkpad } from '../../state/defaults';
import { setWorkpad } from '../../state/actions/workpad';
+// @ts-expect-error
import { setAssets, resetAssets } from '../../state/actions/assets';
+// @ts-expect-error
import { setPage } from '../../state/actions/pages';
import { getWorkpad } from '../../state/selectors/workpad';
+// @ts-expect-error
import { setZoomScale } from '../../state/actions/transient';
+import { ErrorStrings } from '../../../i18n';
import { WorkpadApp } from './workpad_app';
+import { State } from '../../../types';
const { workpadRoutes: strings } = ErrorStrings;
@@ -25,7 +32,8 @@ export const routes = [
{
name: 'createWorkpad',
path: '/create',
- action: (dispatch) => async ({ router }) => {
+ // @ts-expect-error Fix when Router is typed.
+ action: (dispatch: Dispatch) => async ({ router }) => {
const newWorkpad = getDefaultWorkpad();
try {
await workpadService.create(newWorkpad);
@@ -46,7 +54,13 @@ export const routes = [
{
name: 'loadWorkpad',
path: '/:id(/page/:page)',
- action: (dispatch, getState) => async ({ params, router }) => {
+ action: (dispatch: Dispatch, getState: () => State) => async ({
+ params,
+ // @ts-expect-error Fix when Router is typed.
+ router,
+ }: {
+ params: { id: string; page?: string };
+ }) => {
// load workpad if given a new id via url param
const state = getState();
const currentWorkpad = getWorkpad(state);
@@ -70,10 +84,10 @@ export const routes = [
// fetch the workpad again, to get changes
const workpad = getWorkpad(getState());
- const pageNumber = parseInt(params.page, 10);
+ const pageNumber = params.page ? parseInt(params.page, 10) : null;
// no page provided, append current page to url
- if (isNaN(pageNumber)) {
+ if (!pageNumber || isNaN(pageNumber)) {
return router.redirectTo('loadWorkpad', { id: workpad.id, page: workpad.page + 1 });
}
diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js
deleted file mode 100644
index ac50cd3fb99b6..0000000000000
--- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { connect } from 'react-redux';
-import { compose, branch, renderComponent } from 'recompose';
-import { selectToplevelNodes } from '../../../state/actions/transient';
-import { canUserWrite, getAppReady } from '../../../state/selectors/app';
-import { getWorkpad, isWriteable } from '../../../state/selectors/workpad';
-import { LoadWorkpad } from './load_workpad';
-import { WorkpadApp as Component } from './workpad_app';
-import { withElementsLoadedTelemetry } from './workpad_telemetry';
-
-export { WORKPAD_CONTAINER_ID } from './workpad_app';
-
-const mapStateToProps = (state) => {
- const appReady = getAppReady(state);
-
- return {
- isWriteable: isWriteable(state) && canUserWrite(state),
- appReady: typeof appReady === 'object' ? appReady : { ready: appReady },
- workpad: getWorkpad(state),
- };
-};
-
-const mapDispatchToProps = (dispatch) => ({
- deselectElement(ev) {
- ev && ev.stopPropagation();
- dispatch(selectToplevelNodes([]));
- },
-});
-
-const branches = [branch(({ workpad }) => workpad == null, renderComponent(LoadWorkpad))];
-
-export const WorkpadApp = compose(
- connect(mapStateToProps, mapDispatchToProps),
- ...branches,
- withElementsLoadedTelemetry
-)(Component);
diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.ts b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.ts
new file mode 100644
index 0000000000000..a00bf855ba376
--- /dev/null
+++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { WorkpadApp } from './workpad_app';
+export { WorkpadApp as WorkpadAppComponent } from './workpad_app.component';
diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.component.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.component.tsx
new file mode 100644
index 0000000000000..791f40f0219cd
--- /dev/null
+++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.component.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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, { FC, MouseEventHandler, useRef } from 'react';
+import PropTypes from 'prop-types';
+import { Sidebar } from '../../../components/sidebar';
+import { Toolbar } from '../../../components/toolbar';
+// @ts-expect-error Untyped local
+import { Workpad } from '../../../components/workpad';
+import { WorkpadHeader } from '../../../components/workpad_header';
+import { CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR } from '../../../../common/lib/constants';
+import { CommitFn } from '../../../../types';
+
+export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer';
+
+interface Props {
+ deselectElement?: MouseEventHandler;
+ isWriteable: boolean;
+}
+
+export const WorkpadApp: FC = ({ deselectElement, isWriteable }) => {
+ const interactivePageLayout = useRef(null); // future versions may enable editing on multiple pages => use array then
+
+ const registerLayout = (newLayout: CommitFn) => {
+ if (interactivePageLayout.current !== newLayout) {
+ interactivePageLayout.current = newLayout;
+ }
+ };
+
+ const unregisterLayout = (oldLayout: CommitFn) => {
+ if (interactivePageLayout.current === oldLayout) {
+ interactivePageLayout.current = null;
+ }
+ };
+
+ const commit = interactivePageLayout.current || (() => {});
+
+ return (
+
+
+
+
+
+
+
+
+
+ {/* NOTE: canvasWorkpadContainer is used for exporting */}
+
+
+
+
+
+
+ {isWriteable && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+WorkpadApp.propTypes = {
+ isWriteable: PropTypes.bool.isRequired,
+ deselectElement: PropTypes.func,
+};
diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js
deleted file mode 100644
index fc3ac9922355a..0000000000000
--- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Sidebar } from '../../../components/sidebar';
-import { Toolbar } from '../../../components/toolbar';
-import { Workpad } from '../../../components/workpad';
-import { WorkpadHeader } from '../../../components/workpad_header';
-import { CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR } from '../../../../common/lib/constants';
-
-export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer';
-
-export class WorkpadApp extends React.PureComponent {
- static propTypes = {
- isWriteable: PropTypes.bool.isRequired,
- deselectElement: PropTypes.func,
- };
-
- interactivePageLayout = null; // future versions may enable editing on multiple pages => use array then
-
- registerLayout(newLayout) {
- if (this.interactivePageLayout !== newLayout) {
- this.interactivePageLayout = newLayout;
- }
- }
-
- unregisterLayout(oldLayout) {
- if (this.interactivePageLayout === oldLayout) {
- this.interactivePageLayout = null;
- }
- }
-
- render() {
- const { isWriteable, deselectElement } = this.props;
-
- return (
-
-
-
-
-
- {})} />
-
-
-
- {/* NOTE: canvasWorkpadContainer is used for exporting */}
-
-
-
-
-
-
- {isWriteable && (
-
-
-
- )}
-
-
-
-
-
-
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.ts b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.ts
new file mode 100644
index 0000000000000..46f2efaf5e7d2
--- /dev/null
+++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { MouseEventHandler } from 'react';
+import { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+// @ts-expect-error untyped local
+import { selectToplevelNodes } from '../../../state/actions/transient';
+import { canUserWrite } from '../../../state/selectors/app';
+import { getWorkpad, isWriteable } from '../../../state/selectors/workpad';
+import { WorkpadApp as Component } from './workpad_app.component';
+import { withElementsLoadedTelemetry } from './workpad_telemetry';
+import { State } from '../../../../types';
+
+export { WORKPAD_CONTAINER_ID } from './workpad_app.component';
+
+const mapDispatchToProps = (dispatch: Dispatch): { deselectElement: MouseEventHandler } => ({
+ deselectElement: (ev) => {
+ ev.stopPropagation();
+ dispatch(selectToplevelNodes([]));
+ },
+});
+
+export const WorkpadApp = connect(
+ (state: State) => ({
+ isWriteable: isWriteable(state) && canUserWrite(state),
+ workpad: getWorkpad(state),
+ }),
+ mapDispatchToProps
+)(withElementsLoadedTelemetry(Component));
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx
index eb4b451896b46..b1e87ca67f5e5 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx
@@ -18,22 +18,25 @@ import { EditMenu } from './edit_menu';
import { ElementMenu } from './element_menu';
import { ShareMenu } from './share_menu';
import { ViewMenu } from './view_menu';
+import { CommitFn } from '../../../types';
const { WorkpadHeader: strings } = ComponentStrings;
export interface Props {
isWriteable: boolean;
- toggleWriteable: () => void;
canUserWrite: boolean;
- commit: (type: string, payload: any) => any;
+ commit: CommitFn;
+ onSetWriteable?: (writeable: boolean) => void;
}
export const WorkpadHeader: FunctionComponent = ({
isWriteable,
canUserWrite,
- toggleWriteable,
commit,
+ onSetWriteable = () => {},
}) => {
+ const toggleWriteable = () => onSetWriteable(!isWriteable);
+
const keyHandler = (action: string) => {
if (action === 'EDITING') {
toggleWriteable();
@@ -145,6 +148,7 @@ export const WorkpadHeader: FunctionComponent = ({
WorkpadHeader.propTypes = {
isWriteable: PropTypes.bool,
- toggleWriteable: PropTypes.func,
+ commit: PropTypes.func.isRequired,
+ onSetWriteable: PropTypes.func,
canUserWrite: PropTypes.bool,
};
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx
index 1f630040b0c36..0661aa4be4313 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx
@@ -10,37 +10,16 @@ import { canUserWrite } from '../../state/selectors/app';
import { getSelectedPage, isWriteable } from '../../state/selectors/workpad';
import { setWriteable } from '../../state/actions/workpad';
import { State } from '../../../types';
-import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header.component';
+import { WorkpadHeader as Component } from './workpad_header.component';
-interface StateProps {
- isWriteable: boolean;
- canUserWrite: boolean;
- selectedPage: string;
-}
-
-interface DispatchProps {
- setWriteable: (isWorkpadWriteable: boolean) => void;
-}
-
-const mapStateToProps = (state: State): StateProps => ({
+const mapStateToProps = (state: State) => ({
isWriteable: isWriteable(state) && canUserWrite(state),
canUserWrite: canUserWrite(state),
selectedPage: getSelectedPage(state),
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
- setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)),
-});
-
-const mergeProps = (
- stateProps: StateProps,
- dispatchProps: DispatchProps,
- ownProps: ComponentProps
-): ComponentProps => ({
- ...stateProps,
- ...dispatchProps,
- ...ownProps,
- toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable),
+ onSetWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)),
});
-export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component);
+export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps)(Component);
diff --git a/x-pack/plugins/canvas/types/canvas.ts b/x-pack/plugins/canvas/types/canvas.ts
index cc07f498f1eec..6b3f9ad3e8043 100644
--- a/x-pack/plugins/canvas/types/canvas.ts
+++ b/x-pack/plugins/canvas/types/canvas.ts
@@ -76,3 +76,7 @@ export interface CanvasWorkpadBoundingBox {
top: number;
bottom: number;
}
+
+export type LayoutState = any;
+
+export type CommitFn = (type: string, payload: any) => LayoutState;
diff --git a/x-pack/plugins/discover_enhanced/server/config.ts b/x-pack/plugins/discover_enhanced/server/config.ts
index becbdee1bfe40..3e5e29e8c7de7 100644
--- a/x-pack/plugins/discover_enhanced/server/config.ts
+++ b/x-pack/plugins/discover_enhanced/server/config.ts
@@ -10,7 +10,7 @@ import { PluginConfigDescriptor } from '../../../../src/core/server';
export const configSchema = schema.object({
actions: schema.object({
exploreDataInChart: schema.object({
- enabled: schema.boolean({ defaultValue: true }),
+ enabled: schema.boolean({ defaultValue: false }),
}),
}),
});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js
index 943f663a025d8..c6da347ed8cfe 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js
@@ -5,16 +5,23 @@
*/
import React from 'react';
+import { act } from 'react-dom/test-utils';
import moment from 'moment-timezone';
import { Provider } from 'react-redux';
// axios has a $http like interface so using it to simulate $http
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
-import sinon from 'sinon';
import { findTestSubject } from '@elastic/eui/lib/test';
+import { init as initHttpRequests } from './helpers/http_requests';
+import {
+ notificationServiceMock,
+ fatalErrorsServiceMock,
+} from '../../../../../src/core/public/mocks';
+import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks';
+
import { mountWithIntl } from '../../../../test_utils/enzyme_helpers';
-import { fetchedPolicies, fetchedNodes } from '../../public/application/store/actions';
+import { fetchedPolicies } from '../../public/application/store/actions';
import { indexLifecycleManagementStore } from '../../public/application/store';
import { EditPolicy } from '../../public/application/sections/edit_policy';
import { init as initHttp } from '../../public/application/services/http';
@@ -33,15 +40,17 @@ import {
policyNameMustBeDifferentErrorMessage,
policyNameAlreadyUsedErrorMessage,
maximumDocumentsRequiredMessage,
-} from '../../public/application/store/selectors/lifecycle';
+} from '../../public/application/store/selectors';
-initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path);
-initUiMetric({ reportUiStats: () => {} });
-initNotification({
- addDanger: () => {},
-});
+initHttp(axios.create({ adapter: axiosXhrAdapter }));
+initUiMetric(usageCollectionPluginMock.createSetupContract());
+initNotification(
+ notificationServiceMock.createSetupContract().toasts,
+ fatalErrorsServiceMock.createSetupContract()
+);
let server;
+let httpRequestsMockHelpers;
let store;
const policy = {
phases: {
@@ -70,9 +79,11 @@ for (let i = 0; i < 105; i++) {
window.scrollTo = jest.fn();
window.TextEncoder = null;
let component;
-const activatePhase = (rendered, phase) => {
+const activatePhase = async (rendered, phase) => {
const testSubject = `enablePhaseSwitch-${phase}`;
- findTestSubject(rendered, testSubject).simulate('click');
+ await act(async () => {
+ await findTestSubject(rendered, testSubject).simulate('click');
+ });
rendered.update();
};
const expectedErrorMessages = (rendered, expectedErrorMessages) => {
@@ -120,16 +131,13 @@ describe('edit policy', () => {
store = indexLifecycleManagementStore();
component = (
- {}} />
+ {} }} getUrlForApp={() => {}} />
);
store.dispatch(fetchedPolicies(policies));
- server = sinon.fakeServer.create();
- server.respondWith('/api/index_lifecycle_management/policies', [
- 200,
- { 'Content-Type': 'application/json' },
- JSON.stringify(policies),
- ]);
+ ({ server, httpRequestsMockHelpers } = initHttpRequests());
+
+ httpRequestsMockHelpers.setPoliciesResponse(policies);
});
describe('top level form', () => {
test('should show error when trying to save empty form', () => {
@@ -242,48 +250,53 @@ describe('edit policy', () => {
});
});
describe('warm phase', () => {
- test('should show number required error when trying to save empty warm phase', () => {
+ beforeEach(() => {
+ server.respondImmediately = true;
+ httpRequestsMockHelpers.setNodesListResponse({});
+ });
+
+ test('should show number required error when trying to save empty warm phase', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', '');
save(rendered);
expectedErrorMessages(rendered, [numberRequiredMessage]);
});
- test('should allow 0 for phase timing', () => {
+ test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', 0);
save(rendered);
expectedErrorMessages(rendered, []);
});
- test('should show positive number required error when trying to save warm phase with -1 for after', () => {
+ test('should show positive number required error when trying to save warm phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', -1);
save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
- test('should show positive number required error when trying to save warm phase with -1 for index priority', () => {
+ test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', 1);
setPhaseIndexPriority(rendered, 'warm', -1);
save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
- test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', () => {
+ test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
findTestSubject(rendered, 'shrinkSwitch').simulate('click');
rendered.update();
setPhaseAfter(rendered, 'warm', 1);
@@ -293,11 +306,11 @@ describe('edit policy', () => {
save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
- test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', () => {
+ test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', 1);
findTestSubject(rendered, 'shrinkSwitch').simulate('click');
rendered.update();
@@ -307,11 +320,11 @@ describe('edit policy', () => {
save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
- test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', () => {
+ test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', 1);
findTestSubject(rendered, 'forceMergeSwitch').simulate('click');
rendered.update();
@@ -321,11 +334,11 @@ describe('edit policy', () => {
save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
- test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', () => {
+ test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
setPhaseAfter(rendered, 'warm', 1);
findTestSubject(rendered, 'forceMergeSwitch').simulate('click');
rendered.update();
@@ -335,43 +348,43 @@ describe('edit policy', () => {
save(rendered);
expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]);
});
- test('should show spinner for node attributes input when loading', () => {
+ test('should show spinner for node attributes input when loading', async () => {
+ server.respondImmediately = false;
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy();
});
- test('should show warning instead of node attributes input when none exist', () => {
- store.dispatch(fetchedNodes({}));
+ test('should show warning instead of node attributes input when none exist', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy();
expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy();
});
- test('should show node attributes input when attributes exist', () => {
- store.dispatch(fetchedNodes({ 'attribute:true': ['node1'] }));
+ test('should show node attributes input when attributes exist', async () => {
+ httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] });
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm');
expect(nodeAttributesSelect.exists()).toBeTruthy();
expect(nodeAttributesSelect.find('option').length).toBe(2);
});
- test('should show view node attributes link when attribute selected and show flyout when clicked', () => {
- store.dispatch(fetchedNodes({ 'attribute:true': ['node1'] }));
+ test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
+ httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] });
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'warm');
+ await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm');
@@ -388,61 +401,65 @@ describe('edit policy', () => {
});
});
describe('cold phase', () => {
- test('should allow 0 for phase timing', () => {
+ beforeEach(() => {
+ server.respondImmediately = true;
+ httpRequestsMockHelpers.setNodesListResponse({});
+ });
+ test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'cold');
+ await activatePhase(rendered, 'cold');
setPhaseAfter(rendered, 'cold', 0);
save(rendered);
expectedErrorMessages(rendered, []);
});
- test('should show positive number required error when trying to save cold phase with -1 for after', () => {
+ test('should show positive number required error when trying to save cold phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'cold');
+ await activatePhase(rendered, 'cold');
setPhaseAfter(rendered, 'cold', -1);
save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
});
- test('should show spinner for node attributes input when loading', () => {
+ test('should show spinner for node attributes input when loading', async () => {
+ server.respondImmediately = false;
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'cold');
+ await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy();
});
- test('should show warning instead of node attributes input when none exist', () => {
- store.dispatch(fetchedNodes({}));
+ test('should show warning instead of node attributes input when none exist', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'cold');
+ await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy();
expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy();
});
- test('should show node attributes input when attributes exist', () => {
- store.dispatch(fetchedNodes({ 'attribute:true': ['node1'] }));
+ test('should show node attributes input when attributes exist', async () => {
+ httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] });
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'cold');
+ await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold');
expect(nodeAttributesSelect.exists()).toBeTruthy();
expect(nodeAttributesSelect.find('option').length).toBe(2);
});
- test('should show view node attributes link when attribute selected and show flyout when clicked', () => {
- store.dispatch(fetchedNodes({ 'attribute:true': ['node1'] }));
+ test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
+ httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] });
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'cold');
+ await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold');
@@ -457,11 +474,11 @@ describe('edit policy', () => {
rendered.update();
expect(rendered.find('.euiFlyout').exists()).toBeTruthy();
});
- test('should show positive number required error when trying to save with -1 for index priority', () => {
+ test('should show positive number required error when trying to save with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'cold');
+ await activatePhase(rendered, 'cold');
setPhaseAfter(rendered, 'cold', 1);
setPhaseIndexPriority(rendered, 'cold', -1);
save(rendered);
@@ -469,20 +486,20 @@ describe('edit policy', () => {
});
});
describe('delete phase', () => {
- test('should allow 0 for phase timing', () => {
+ test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'delete');
+ await activatePhase(rendered, 'delete');
setPhaseAfter(rendered, 'delete', 0);
save(rendered);
expectedErrorMessages(rendered, []);
});
- test('should show positive number required error when trying to save delete phase with -1 for after', () => {
+ test('should show positive number required error when trying to save delete phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
noRollover(rendered);
setPolicyName(rendered, 'mypolicy');
- activatePhase(rendered, 'delete');
+ await activatePhase(rendered, 'delete');
setPhaseAfter(rendered, 'delete', -1);
save(rendered);
expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts
new file mode 100644
index 0000000000000..b5c941beef181
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 sinon, { SinonFakeServer } from 'sinon';
+
+type HttpResponse = Record | any[];
+
+const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
+ const setPoliciesResponse = (response: HttpResponse = []) => {
+ server.respondWith('/api/index_lifecycle_management/policies', [
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(response),
+ ]);
+ };
+
+ const setNodesListResponse = (response: HttpResponse = []) => {
+ server.respondWith('/api/index_lifecycle_management/nodes/list', [
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(response),
+ ]);
+ };
+
+ return {
+ setPoliciesResponse,
+ setNodesListResponse,
+ };
+};
+
+export const init = () => {
+ const server = sinon.fakeServer.create();
+
+ // Define default response for unhandled requests.
+ // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry,
+ // and we can mock them all with a 200 instead of mocking each one individually.
+ server.respondWith([200, {}, 'DefaultSinonMockServerResponse']);
+
+ const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server);
+
+ return {
+ server,
+ httpRequestsMockHelpers,
+ };
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js
deleted file mode 100644
index 2284b9e39835c..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { EuiLink } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-import { createDocLink } from '../../services/documentation';
-
-export class LearnMoreLink extends React.PureComponent {
- render() {
- const { href, docPath, text } = this.props;
- let url;
- if (docPath) {
- url = createDocLink(docPath);
- } else {
- url = href;
- }
- const content = text ? (
- text
- ) : (
-
- );
- return (
-
- {content}
-
- );
- }
-}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx
new file mode 100644
index 0000000000000..623ff982438d7
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { ReactNode } from 'react';
+import { EuiLink } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { createDocLink } from '../../services/documentation';
+
+interface Props {
+ docPath: string;
+ text?: ReactNode;
+}
+
+export const LearnMoreLink: React.FunctionComponent = ({ docPath, text }) => {
+ const content = text ? (
+ text
+ ) : (
+
+ );
+ return (
+
+ {content}
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts
similarity index 79%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts
index 9138c6a30cfad..4675ab46ee501 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.js
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { NodeAllocation } from './node_allocation.container';
+export { NodeAllocation } from './node_allocation';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js
deleted file mode 100644
index 0ddfcbb940aa4..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.container.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { connect } from 'react-redux';
-
-import { getNodeOptions } from '../../../../store/selectors';
-import { fetchNodes } from '../../../../store/actions';
-import { NodeAllocation as PresentationComponent } from './node_allocation';
-
-export const NodeAllocation = connect(
- (state) => ({
- nodeOptions: getNodeOptions(state),
- }),
- {
- fetchNodes,
- }
-)(PresentationComponent);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js
deleted file mode 100644
index 95c1878776688..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.js
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { Component, Fragment } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-import { EuiSelect, EuiButtonEmpty, EuiCallOut, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
-
-import { PHASE_NODE_ATTRS } from '../../../../constants';
-import { LearnMoreLink } from '../../../components/learn_more_link';
-import { ErrableFormRow } from '../../form_errors';
-
-const learnMoreLinks = (
-
-
-
-
- }
- docPath="shards-allocation.html"
- />
-
-);
-
-export class NodeAllocation extends Component {
- componentDidMount() {
- this.props.fetchNodes();
- }
-
- render() {
- const {
- phase,
- setPhaseData,
- isShowingErrors,
- phaseData,
- showNodeDetailsFlyout,
- nodeOptions,
- errors,
- } = this.props;
- if (!nodeOptions) {
- return (
-
-
-
-
- );
- }
- if (!nodeOptions.length) {
- return (
-
-
- }
- color="warning"
- >
-
- {learnMoreLinks}
-
-
-
-
- );
- }
-
- return (
-
-
- {
- setPhaseData(PHASE_NODE_ATTRS, e.target.value);
- }}
- />
-
- {!!phaseData[PHASE_NODE_ATTRS] ? (
- showNodeDetailsFlyout(phaseData[PHASE_NODE_ATTRS])}
- >
-
-
- ) : (
-
- )}
- {learnMoreLinks}
-
-
- );
- }
-}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx
new file mode 100644
index 0000000000000..208f6b2aa6131
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.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 React, { Fragment } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiSelect,
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiSpacer,
+ EuiLoadingSpinner,
+ EuiButton,
+} from '@elastic/eui';
+
+import { PHASE_NODE_ATTRS } from '../../../../constants';
+import { LearnMoreLink } from '../../../components/learn_more_link';
+import { ErrableFormRow } from '../../form_errors';
+import { useLoadNodes } from '../../../../services/api';
+
+interface Props {
+ phase: string;
+ setPhaseData: (dataKey: string, value: any) => void;
+ showNodeDetailsFlyout: (nodeAttrs: any) => void;
+ errors: any;
+ phaseData: any;
+ isShowingErrors: boolean;
+}
+
+const learnMoreLink = (
+
+
+
+ }
+ docPath="modules-cluster.html#cluster-shard-allocation-settings"
+ />
+
+);
+
+export const NodeAllocation: React.FunctionComponent = ({
+ phase,
+ setPhaseData,
+ showNodeDetailsFlyout,
+ errors,
+ phaseData,
+ isShowingErrors,
+}) => {
+ const { isLoading, data: nodes, error, sendRequest } = useLoadNodes();
+
+ if (isLoading) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (error) {
+ const { statusCode, message } = error;
+ return (
+
+
+ }
+ color="danger"
+ >
+
-
- >
- );
- }
-
- /**
- * Could happen if user e.g. loads a URL with a bad crumbEvent
- */
- if (!relatedEventToShowDetailsFor) {
- const errString = i18n.translate(
- 'xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing',
- {
- defaultMessage: 'Related event not found.',
- }
- );
- return (
-
- );
- }
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {sections.map(({ sectionTitle, entries }, index) => {
- return (
-
- {index === 0 ? null : }
-
-
-
- {sectionTitle}
-
-
-
-
-
-
- {index === sections.length - 1 ? null : }
-
- );
- })}
- >
- );
-});
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo, useMemo, useEffect, Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from '@elastic/eui';
+import styled from 'styled-components';
+import { useSelector } from 'react-redux';
+import { FormattedMessage } from 'react-intl';
+import {
+ CrumbInfo,
+ formatDate,
+ StyledBreadcrumbs,
+ BoldCode,
+ StyledTime,
+} from './panel_content_utilities';
+import * as event from '../../../../common/endpoint/models/event';
+import { ResolverEvent } from '../../../../common/endpoint/types';
+import * as selectors from '../../store/selectors';
+import { useResolverDispatch } from '../use_resolver_dispatch';
+import { PanelContentError } from './panel_content_error';
+
+/**
+ * A helper function to turn objects into EuiDescriptionList entries.
+ * This reflects the strategy of more or less "dumping" metadata for related processes
+ * in description lists with little/no 'prettification'. This has the obvious drawback of
+ * data perhaps appearing inscrutable/daunting, but the benefit of presenting these fields
+ * to the user "as they occur" in ECS, which may help them with e.g. EQL queries.
+ *
+ * Given an object like: {a:{b: 1}, c: 'd'} it will yield title/description entries like so:
+ * {title: "a.b", description: "1"}, {title: "c", description: "d"}
+ *
+ * @param {object} obj The object to turn into `