diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 692859cb9ecda0..0c69b3b7dc87ad 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -388,7 +388,7 @@ In the screenshot below, you'll notice the URL is `localhost:9876/debug.html`. Y
### Unit Testing Plugins
-This should work super if you're using the [Kibana plugin generator](https://github.com/elastic/generator-kibana-plugin). If you're not using the generator, well, you're on your own. We suggest you look at how the generator works.
+This should work super if you're using the [Kibana plugin generator](https://github.com/elastic/kibana/tree/master/packages/kbn-plugin-generator). If you're not using the generator, well, you're on your own. We suggest you look at how the generator works.
To run the tests for just your particular plugin run the following command from your plugin:
diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md
index eab2f5ea642997..a34c13b944125b 100644
--- a/docs/development/core/public/kibana-plugin-public.coresetup.md
+++ b/docs/development/core/public/kibana-plugin-public.coresetup.md
@@ -21,5 +21,6 @@ export interface CoreSetup
| [i18n](./kibana-plugin-public.coresetup.i18n.md) | I18nSetup
| |
| [injectedMetadata](./kibana-plugin-public.coresetup.injectedmetadata.md) | InjectedMetadataSetup
| |
| [notifications](./kibana-plugin-public.coresetup.notifications.md) | NotificationsSetup
| |
+| [overlays](./kibana-plugin-public.coresetup.overlays.md) | OverlaySetup
| |
| [uiSettings](./kibana-plugin-public.coresetup.uisettings.md) | UiSettingsSetup
| |
diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.overlays.md b/docs/development/core/public/kibana-plugin-public.coresetup.overlays.md
new file mode 100644
index 00000000000000..e55828a9031206
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.coresetup.overlays.md
@@ -0,0 +1,9 @@
+[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [overlays](./kibana-plugin-public.coresetup.overlays.md)
+
+## CoreSetup.overlays property
+
+Signature:
+
+```typescript
+overlays: OverlaySetup;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.flyoutref.close.md b/docs/development/core/public/kibana-plugin-public.flyoutref.close.md
new file mode 100644
index 00000000000000..500752987f1804
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.flyoutref.close.md
@@ -0,0 +1,15 @@
+[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FlyoutRef](./kibana-plugin-public.flyoutref.md) > [close](./kibana-plugin-public.flyoutref.close.md)
+
+## FlyoutRef.close() method
+
+Closes the referenced flyout if it's still open which in turn will resolve the `onClose` Promise. If the flyout had already been closed this method does nothing.
+
+Signature:
+
+```typescript
+close(): Promise;
+```
+Returns:
+
+`Promise`
+
diff --git a/docs/development/core/public/kibana-plugin-public.flyoutref.md b/docs/development/core/public/kibana-plugin-public.flyoutref.md
new file mode 100644
index 00000000000000..b529ad9f594f19
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.flyoutref.md
@@ -0,0 +1,24 @@
+[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FlyoutRef](./kibana-plugin-public.flyoutref.md)
+
+## FlyoutRef class
+
+A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call `close()` when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the `onClose` Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef.
+
+Signature:
+
+```typescript
+export declare class FlyoutRef
+```
+
+## Properties
+
+| Property | Modifiers | Type | Description |
+| --- | --- | --- | --- |
+| [onClose](./kibana-plugin-public.flyoutref.onclose.md) | | Promise<void>
| An Promise that will resolve once this flyout is closed.Flyouts can close from user interaction, calling close()
on the flyout reference or another call to openFlyout()
replacing your flyout. |
+
+## Methods
+
+| Method | Modifiers | Description |
+| --- | --- | --- |
+| [close()](./kibana-plugin-public.flyoutref.close.md) | | Closes the referenced flyout if it's still open which in turn will resolve the onClose
Promise. If the flyout had already been closed this method does nothing. |
+
diff --git a/docs/development/core/public/kibana-plugin-public.flyoutref.onclose.md b/docs/development/core/public/kibana-plugin-public.flyoutref.onclose.md
new file mode 100644
index 00000000000000..dbf578a2b478a4
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.flyoutref.onclose.md
@@ -0,0 +1,13 @@
+[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [FlyoutRef](./kibana-plugin-public.flyoutref.md) > [onClose](./kibana-plugin-public.flyoutref.onclose.md)
+
+## FlyoutRef.onClose property
+
+An Promise that will resolve once this flyout is closed.
+
+Flyouts can close from user interaction, calling `close()` on the flyout reference or another call to `openFlyout()` replacing your flyout.
+
+Signature:
+
+```typescript
+readonly onClose: Promise;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md
index fb55da12b0e796..372592c9f5817f 100644
--- a/docs/development/core/public/kibana-plugin-public.md
+++ b/docs/development/core/public/kibana-plugin-public.md
@@ -6,6 +6,7 @@
| Class | Description |
| --- | --- |
+| [FlyoutRef](./kibana-plugin-public.flyoutref.md) | A FlyoutRef is a reference to an opened flyout panel. It offers methods to close the flyout panel again. If you open a flyout panel you should make sure you call close()
when it should be closed. Since a flyout could also be closed by a user or from another flyout being opened, you must bind to the onClose
Promise on the FlyoutRef instance. The Promise will resolve whenever the flyout was closed at which point you should discard the FlyoutRef. |
| [ToastsSetup](./kibana-plugin-public.toastssetup.md) | |
| [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | |
@@ -16,6 +17,7 @@
| [ChromeBrand](./kibana-plugin-public.chromebrand.md) | |
| [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | |
| [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the start lifecycle |
+| [OverlaySetup](./kibana-plugin-public.overlaysetup.md) | |
| [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a PluginInitializer
. |
| [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer
|
| [PluginSetupContext](./kibana-plugin-public.pluginsetupcontext.md) | The available core services passed to a plugin's Plugin#setup
method. |
diff --git a/docs/development/core/public/kibana-plugin-public.overlaysetup.md b/docs/development/core/public/kibana-plugin-public.overlaysetup.md
new file mode 100644
index 00000000000000..6fba41e089b742
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.overlaysetup.md
@@ -0,0 +1,17 @@
+[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlaySetup](./kibana-plugin-public.overlaysetup.md)
+
+## OverlaySetup interface
+
+
+Signature:
+
+```typescript
+export interface OverlaySetup
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [openFlyout](./kibana-plugin-public.overlaysetup.openflyout.md) | (flyoutChildren: React.ReactNode, flyoutProps?: {`
` closeButtonAriaLabel?: string;`
` 'data-test-subj'?: string;`
` }) => FlyoutRef
| |
+
diff --git a/docs/development/core/public/kibana-plugin-public.overlaysetup.openflyout.md b/docs/development/core/public/kibana-plugin-public.overlaysetup.openflyout.md
new file mode 100644
index 00000000000000..609bd6dd60a26a
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.overlaysetup.openflyout.md
@@ -0,0 +1,12 @@
+[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlaySetup](./kibana-plugin-public.overlaysetup.md) > [openFlyout](./kibana-plugin-public.overlaysetup.openflyout.md)
+
+## OverlaySetup.openFlyout property
+
+Signature:
+
+```typescript
+openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
+ closeButtonAriaLabel?: string;
+ 'data-test-subj'?: string;
+ }) => FlyoutRef;
+```
diff --git a/docs/monitoring/monitoring-metricbeat.asciidoc b/docs/monitoring/monitoring-metricbeat.asciidoc
index e18ba551bfd1fb..f009710792922e 100644
--- a/docs/monitoring/monitoring-metricbeat.asciidoc
+++ b/docs/monitoring/monitoring-metricbeat.asciidoc
@@ -90,7 +90,7 @@ run the following command:
["source","sh",subs="attributes,callouts"]
----------------------------------------------------------------------
-metricbeat modules enable kibana
+metricbeat modules enable kibana-xpack
----------------------------------------------------------------------
For more information, see
@@ -98,30 +98,14 @@ For more information, see
{metricbeat-ref}/metricbeat-module-kibana.html[{kib} module].
--
-. Configure the {kib} module in {metricbeat}. +
-+
---
-You must specify the following settings in the `modules.d/kibana.yml` file:
-
-[source,yaml]
-----------------------------------
-- module: kibana
- metricsets:
- - stats
- period: 10s
- hosts: ["http://localhost:5601"] <1>
- xpack.enabled: true <2>
-----------------------------------
-<1> This setting identifies the host and port number that are used to access {kib}.
-<2> This setting ensures that {kib} can read the monitoring data successfully.
-That is to say, it's stored in the same location and format as monitoring data
-that is sent by {ref}/es-monitoring-exporters.html[exporters].
---
+. By default the module will collect {kib} monitoring metrics from `http://localhost:5601`.
+If the local {kib} instance has a different address, you must specify it via the `hosts` setting
+in the `modules.d/kibana-xpack.yml` file.
. If the Elastic {security-features} are enabled, you must also provide a user
ID and password so that {metricbeat} can collect metrics successfully.
-... Create a user on the production cluster that has the
+.. Create a user on the production cluster that has the
`remote_monitoring_collector` {stack-ov}/built-in-roles.html[built-in role].
Alternatively, use the `remote_monitoring_user`
{stack-ov}/built-in-users.html[built-in user].
@@ -130,7 +114,7 @@ Alternatively, use the `remote_monitoring_user`
file.
+
--
-For example, add the following settings in the `modules.d/kibana.yml` file:
+For example, add the following settings in the `modules.d/kibana-xpack.yml` file:
[source,yaml]
----------------------------------
@@ -143,7 +127,7 @@ For example, add the following settings in the `modules.d/kibana.yml` file:
. If you configured {kib} to use <>,
you must access it via HTTPS. For example, use a `hosts` setting like
-`https://localhost:5601` in the `modules.d/kibana.yml` file.
+`https://localhost:5601` in the `modules.d/kibana-xpack.yml` file.
. Identify where to send the monitoring data. +
+
diff --git a/package.json b/package.json
index 8dd7a2de2a0c17..a21442104283de 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"uiFramework:documentComponent": "cd packages/kbn-ui-framework && yarn documentComponent",
"kbn:watch": "node scripts/kibana --dev --logging.json=false",
"build:types": "tsc --p tsconfig.types.json",
+ "core:acceptApiChanges": "yarn build:types && node scripts/check_core_api_changes.js --accept",
"kbn:bootstrap": "yarn build:types && node scripts/register_git_hook"
},
"repository": {
@@ -99,7 +100,7 @@
"@babel/polyfill": "^7.2.5",
"@babel/register": "^7.0.0",
"@elastic/datemath": "5.0.2",
- "@elastic/eui": "9.8.0",
+ "@elastic/eui": "9.9.0",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "8.1.1-kibana2",
"@elastic/numeral": "2.3.2",
diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json
index ff68adeac86319..091b019c3ea989 100644
--- a/packages/kbn-es-query/package.json
+++ b/packages/kbn-es-query/package.json
@@ -11,7 +11,8 @@
"kbn:watch": "node scripts/build --source-maps --watch"
},
"dependencies": {
- "lodash": "npm:@elastic/lodash@3.10.1-kibana1"
+ "lodash": "npm:@elastic/lodash@3.10.1-kibana1",
+ "moment-timezone": "^0.5.14"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
diff --git a/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js b/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js
index 879fb1cd6c45ff..5c27a204ef2633 100644
--- a/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js
+++ b/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js
@@ -103,6 +103,36 @@ describe('build query', function () {
expect(result).to.eql(expectedResult);
});
+ it('should use the default time zone set in the Advanced Settings in queries and filters', function () {
+ const queries = [
+ { query: '@timestamp:"2019-03-23T13:18:00"', language: 'kuery' },
+ { query: '@timestamp:"2019-03-23T13:18:00"', language: 'lucene' }
+ ];
+ const filters = [
+ { match_all: {}, meta: { type: 'match_all' } }
+ ];
+ const config = {
+ allowLeadingWildcards: true,
+ queryStringOptions: {},
+ ignoreFilterIfFieldNotInIndex: false,
+ dateFormatTZ: 'Africa/Johannesburg',
+ };
+
+ const expectedResult = {
+ bool: {
+ must: [
+ decorateQuery(luceneStringToDsl('@timestamp:"2019-03-23T13:18:00"'), config.queryStringOptions, config.dateFormatTZ),
+ { match_all: {} }
+ ],
+ filter: [toElasticsearchQuery(fromKueryExpression('@timestamp:"2019-03-23T13:18:00"'), indexPattern, config)],
+ should: [],
+ must_not: [],
+ }
+ };
+ const result = buildEsQuery(indexPattern, queries, filters, config);
+ expect(result).to.eql(expectedResult);
+ });
+
});
});
diff --git a/packages/kbn-es-query/src/es_query/__tests__/decorate_query.js b/packages/kbn-es-query/src/es_query/__tests__/decorate_query.js
index 447a875e9eadc1..d5978716dac9e2 100644
--- a/packages/kbn-es-query/src/es_query/__tests__/decorate_query.js
+++ b/packages/kbn-es-query/src/es_query/__tests__/decorate_query.js
@@ -29,4 +29,9 @@ describe('Query decorator', function () {
const decoratedQuery = decorateQuery({ query_string: { query: '*' } }, { analyze_wildcard: true });
expect(decoratedQuery).to.eql({ query_string: { query: '*', analyze_wildcard: true } });
});
+
+ it('should add a default of a time_zone parameter if one is provided', function () {
+ const decoratedQuery = decorateQuery({ query_string: { query: '*' } }, { analyze_wildcard: true }, 'America/Phoenix');
+ expect(decoratedQuery).to.eql({ query_string: { query: '*', analyze_wildcard: true, time_zone: 'America/Phoenix' } });
+ });
});
diff --git a/packages/kbn-es-query/src/es_query/__tests__/from_kuery.js b/packages/kbn-es-query/src/es_query/__tests__/from_kuery.js
index 7c285a4416abc8..3041e8a2c06d88 100644
--- a/packages/kbn-es-query/src/es_query/__tests__/from_kuery.js
+++ b/packages/kbn-es-query/src/es_query/__tests__/from_kuery.js
@@ -60,6 +60,31 @@ describe('build query', function () {
);
});
+
+ it('should accept a specific date format for a kuery query into an ES query in the bool\'s filter clause', function () {
+ const queries = [{ query: '@timestamp:"2018-04-03T19:04:17"', language: 'kuery' }];
+
+ const expectedESQueries = queries.map(query => {
+ return toElasticsearchQuery(fromKueryExpression(query.query), indexPattern, { dateFormatTZ: 'America/Phoenix' });
+ });
+
+ const result = buildQueryFromKuery(indexPattern, queries, true, 'America/Phoenix');
+
+ expect(result.filter).to.eql(expectedESQueries);
+ });
+
+ it('should gracefully handle date queries when no date format is provided', function () {
+ const queries = [{ query: '@timestamp:"2018-04-03T19:04:17Z"', language: 'kuery' }];
+
+ const expectedESQueries = queries.map(query => {
+ return toElasticsearchQuery(fromKueryExpression(query.query), indexPattern);
+ });
+
+ const result = buildQueryFromKuery(indexPattern, queries, true);
+
+ expect(result.filter).to.eql(expectedESQueries);
+ });
+
});
});
diff --git a/packages/kbn-es-query/src/es_query/__tests__/from_lucene.js b/packages/kbn-es-query/src/es_query/__tests__/from_lucene.js
index 7a4b6f7b359f5a..4361659021bd5a 100644
--- a/packages/kbn-es-query/src/es_query/__tests__/from_lucene.js
+++ b/packages/kbn-es-query/src/es_query/__tests__/from_lucene.js
@@ -66,4 +66,22 @@ describe('build query', function () {
});
+ it('should accept a date format in the decorated queries and combine that into the bool\'s must clause', function () {
+ const queries = [
+ { query: 'foo:bar', language: 'lucene' },
+ { query: 'bar:baz', language: 'lucene' },
+ ];
+ const dateFormatTZ = 'America/Phoenix';
+
+ const expectedESQueries = queries.map(
+ (query) => {
+ return decorateQuery(luceneStringToDsl(query.query), {}, dateFormatTZ);
+ }
+ );
+
+ const result = buildQueryFromLucene(queries, {}, dateFormatTZ);
+
+ expect(result.must).to.eql(expectedESQueries);
+ });
+
});
diff --git a/packages/kbn-es-query/src/es_query/__tests__/get_es_query_config.js b/packages/kbn-es-query/src/es_query/__tests__/get_es_query_config.js
new file mode 100644
index 00000000000000..8ccb04dd4b25a2
--- /dev/null
+++ b/packages/kbn-es-query/src/es_query/__tests__/get_es_query_config.js
@@ -0,0 +1,66 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import expect from '@kbn/expect';
+import { getEsQueryConfig } from '../get_es_query_config';
+
+const config = {
+ get(item) {
+ return config[item];
+ },
+ 'query:allowLeadingWildcards': {
+ allowLeadingWildcards: true,
+ },
+ 'query:queryString:options': {
+ queryStringOptions: {},
+ },
+ 'courier:ignoreFilterIfFieldNotInIndex': {
+ ignoreFilterIfFieldNotInIndex: true,
+ },
+ 'dateFormat:tz': {
+ dateFormatTZ: 'Browser',
+ },
+};
+
+describe('getEsQueryConfig', function () {
+ it('should return the parameters of an Elasticsearch query config requested', function () {
+ const result = getEsQueryConfig(config);
+ const expected = {
+ allowLeadingWildcards: {
+ allowLeadingWildcards: true,
+ },
+ dateFormatTZ: {
+ dateFormatTZ: 'Browser',
+ },
+ ignoreFilterIfFieldNotInIndex: {
+ ignoreFilterIfFieldNotInIndex: true,
+ },
+ queryStringOptions: {
+ queryStringOptions: {},
+ },
+ };
+ expect(result).to.eql(expected);
+ expect(result).to.have.keys(
+ 'allowLeadingWildcards',
+ 'dateFormatTZ',
+ 'ignoreFilterIfFieldNotInIndex',
+ 'queryStringOptions'
+ );
+ });
+});
diff --git a/packages/kbn-es-query/src/es_query/build_es_query.js b/packages/kbn-es-query/src/es_query/build_es_query.js
index 556bd8d52c9acf..d17147761d8bcd 100644
--- a/packages/kbn-es-query/src/es_query/build_es_query.js
+++ b/packages/kbn-es-query/src/es_query/build_es_query.js
@@ -28,6 +28,7 @@ import { buildQueryFromLucene } from './from_lucene';
* @param filters - a filter object or array of filter objects
* @param config - an objects with query:allowLeadingWildcards and query:queryString:options UI
* settings in form of { allowLeadingWildcards, queryStringOptions }
+ * config contains dateformat:tz
*/
export function buildEsQuery(
indexPattern,
@@ -37,15 +38,15 @@ export function buildEsQuery(
allowLeadingWildcards: false,
queryStringOptions: {},
ignoreFilterIfFieldNotInIndex: false,
+ dateFormatTZ: null,
}) {
queries = Array.isArray(queries) ? queries : [queries];
filters = Array.isArray(filters) ? filters : [filters];
const validQueries = queries.filter((query) => has(query, 'query'));
const queriesByLanguage = groupBy(validQueries, 'language');
-
- const kueryQuery = buildQueryFromKuery(indexPattern, queriesByLanguage.kuery, config.allowLeadingWildcards);
- const luceneQuery = buildQueryFromLucene(queriesByLanguage.lucene, config.queryStringOptions);
+ const kueryQuery = buildQueryFromKuery(indexPattern, queriesByLanguage.kuery, config.allowLeadingWildcards, config.dateFormatTZ);
+ const luceneQuery = buildQueryFromLucene(queriesByLanguage.lucene, config.queryStringOptions, config.dateFormatTZ);
const filterQuery = buildQueryFromFilters(filters, indexPattern, config.ignoreFilterIfFieldNotInIndex);
return {
diff --git a/packages/kbn-es-query/src/es_query/decorate_query.js b/packages/kbn-es-query/src/es_query/decorate_query.js
index ff7c3ae80c690e..8104707e0298a5 100644
--- a/packages/kbn-es-query/src/es_query/decorate_query.js
+++ b/packages/kbn-es-query/src/es_query/decorate_query.js
@@ -18,16 +18,22 @@
*/
import _ from 'lodash';
+import { getTimeZoneFromSettings } from '../utils/get_time_zone_from_settings';
/**
* Decorate queries with default parameters
* @param query object
* @param queryStringOptions query:queryString:options from UI settings
+ * @param dateFormatTZ dateFormat:tz from UI settings
* @returns {object}
*/
-export function decorateQuery(query, queryStringOptions) {
+
+export function decorateQuery(query, queryStringOptions, dateFormatTZ = null) {
if (_.has(query, 'query_string.query')) {
_.extend(query.query_string, queryStringOptions);
+ if (dateFormatTZ) {
+ _.defaults(query.query_string, { time_zone: getTimeZoneFromSettings(dateFormatTZ) });
+ }
}
return query;
diff --git a/packages/kbn-es-query/src/es_query/from_kuery.js b/packages/kbn-es-query/src/es_query/from_kuery.js
index 9706e82f9bad1f..723003edc46fdd 100644
--- a/packages/kbn-es-query/src/es_query/from_kuery.js
+++ b/packages/kbn-es-query/src/es_query/from_kuery.js
@@ -19,7 +19,7 @@
import { fromLegacyKueryExpression, fromKueryExpression, toElasticsearchQuery, nodeTypes } from '../kuery';
-export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWildcards) {
+export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWildcards, dateFormatTZ = null) {
const queryASTs = queries.map(query => {
try {
return fromKueryExpression(query.query, { allowLeadingWildcards });
@@ -32,12 +32,12 @@ export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWild
throw Error('OutdatedKuerySyntaxError');
}
});
- return buildQuery(indexPattern, queryASTs);
+ return buildQuery(indexPattern, queryASTs, { dateFormatTZ });
}
-function buildQuery(indexPattern, queryASTs) {
+function buildQuery(indexPattern, queryASTs, config = null) {
const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs);
- const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern);
+ const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern, config);
return {
must: [],
filter: [],
diff --git a/packages/kbn-es-query/src/es_query/from_lucene.js b/packages/kbn-es-query/src/es_query/from_lucene.js
index 7d6df6060ba02c..8845fd68efb4d6 100644
--- a/packages/kbn-es-query/src/es_query/from_lucene.js
+++ b/packages/kbn-es-query/src/es_query/from_lucene.js
@@ -21,10 +21,10 @@ import _ from 'lodash';
import { decorateQuery } from './decorate_query';
import { luceneStringToDsl } from './lucene_string_to_dsl';
-export function buildQueryFromLucene(queries, queryStringOptions) {
+export function buildQueryFromLucene(queries, queryStringOptions, dateFormatTZ = null) {
const combinedQueries = _.map(queries, (query) => {
const queryDsl = luceneStringToDsl(query.query);
- return decorateQuery(queryDsl, queryStringOptions);
+ return decorateQuery(queryDsl, queryStringOptions, dateFormatTZ);
});
return {
diff --git a/packages/kbn-es-query/src/es_query/get_es_query_config.js b/packages/kbn-es-query/src/es_query/get_es_query_config.js
index af6ce412f7b0bc..2518b1077462d4 100644
--- a/packages/kbn-es-query/src/es_query/get_es_query_config.js
+++ b/packages/kbn-es-query/src/es_query/get_es_query_config.js
@@ -21,5 +21,6 @@ export function getEsQueryConfig(config) {
const allowLeadingWildcards = config.get('query:allowLeadingWildcards');
const queryStringOptions = config.get('query:queryString:options');
const ignoreFilterIfFieldNotInIndex = config.get('courier:ignoreFilterIfFieldNotInIndex');
- return { allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex };
+ const dateFormatTZ = config.get('dateFormat:tz');
+ return { allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex, dateFormatTZ };
}
diff --git a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js b/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js
index 96039526752d45..c1e27c68a03bde 100644
--- a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js
+++ b/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js
@@ -24,6 +24,7 @@ import indexPatternResponse from '../../../__fixtures__/index_pattern_response.j
// Helpful utility allowing us to test the PEG parser by simply checking for deep equality between
// the nodes the parser generates and the nodes our constructor functions generate.
+
function fromLegacyKueryExpressionNoMeta(text) {
return ast.fromLegacyKueryExpression(text, { includeMetadata: false });
}
@@ -416,6 +417,14 @@ describe('kuery AST API', function () {
expect(ast.toElasticsearchQuery(unknownTypeNode)).to.eql(expected);
});
+ it('should return the given node type\'s ES query representation including a time zone parameter when one is provided', function () {
+ const config = { dateFormatTZ: 'America/Phoenix' };
+ const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"');
+ const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config);
+ const result = ast.toElasticsearchQuery(node, indexPattern, config);
+ expect(result).to.eql(expected);
+ });
+
});
describe('doesKueryExpressionHaveLuceneSyntaxError', function () {
diff --git a/packages/kbn-es-query/src/kuery/ast/ast.js b/packages/kbn-es-query/src/kuery/ast/ast.js
index 39a22eb8a77a8c..7c8c9534aa1405 100644
--- a/packages/kbn-es-query/src/kuery/ast/ast.js
+++ b/packages/kbn-es-query/src/kuery/ast/ast.js
@@ -51,15 +51,19 @@ function fromExpression(expression, parseOptions = {}, parse = parseKuery) {
return parse(expression, parseOptions);
}
-
-// indexPattern isn't required, but if you pass one in, we can be more intelligent
-// about how we craft the queries (e.g. scripted fields)
-export function toElasticsearchQuery(node, indexPattern) {
+/**
+ * @params {String} indexPattern
+ * @params {Object} config - contains the dateFormatTZ
+ *
+ * IndexPattern isn't required, but if you pass one in, we can be more intelligent
+ * about how we craft the queries (e.g. scripted fields)
+ */
+export function toElasticsearchQuery(node, indexPattern, config = {}) {
if (!node || !node.type || !nodeTypes[node.type]) {
return toElasticsearchQuery(nodeTypes.function.buildNode('and', []));
}
- return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern);
+ return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern, config);
}
export function doesKueryExpressionHaveLuceneSyntaxError(expression) {
diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js b/packages/kbn-es-query/src/kuery/functions/__tests__/and.js
index 5a0d3a9cd58b27..07289a878e8c1e 100644
--- a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js
+++ b/packages/kbn-es-query/src/kuery/functions/__tests__/and.js
@@ -23,7 +23,6 @@ import { nodeTypes } from '../../node_types';
import * as ast from '../../ast';
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
-
let indexPattern;
const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx');
@@ -57,8 +56,6 @@ describe('kuery functions', function () {
[childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern))
);
});
-
});
-
});
});
diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js b/packages/kbn-es-query/src/kuery/functions/__tests__/is.js
index 7ceac9b605db4e..18652c9faccb8d 100644
--- a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js
+++ b/packages/kbn-es-query/src/kuery/functions/__tests__/is.js
@@ -199,6 +199,52 @@ describe('kuery functions', function () {
expect(result.bool.should[0]).to.have.key('script');
});
+ it('should support date fields without a dateFormat provided', function () {
+ const expected = {
+ bool: {
+ should: [
+ {
+ range: {
+ '@timestamp': {
+ gte: '2018-04-03T19:04:17',
+ lte: '2018-04-03T19:04:17',
+ }
+ }
+ }
+ ],
+ minimum_should_match: 1
+ }
+ };
+
+ const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"');
+ const result = is.toElasticsearchQuery(node, indexPattern);
+ expect(result).to.eql(expected);
+ });
+
+ it('should support date fields with a dateFormat provided', function () {
+ const config = { dateFormatTZ: 'America/Phoenix' };
+ const expected = {
+ bool: {
+ should: [
+ {
+ range: {
+ '@timestamp': {
+ gte: '2018-04-03T19:04:17',
+ lte: '2018-04-03T19:04:17',
+ time_zone: 'America/Phoenix',
+ }
+ }
+ }
+ ],
+ minimum_should_match: 1
+ }
+ };
+
+ const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"');
+ const result = is.toElasticsearchQuery(node, indexPattern, config);
+ expect(result).to.eql(expected);
+ });
+
});
});
});
diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js b/packages/kbn-es-query/src/kuery/functions/__tests__/not.js
index 6b5b50e15524da..7a2d7fa39c1528 100644
--- a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js
+++ b/packages/kbn-es-query/src/kuery/functions/__tests__/not.js
@@ -23,7 +23,6 @@ import { nodeTypes } from '../../node_types';
import * as ast from '../../ast';
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
-
let indexPattern;
const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg');
@@ -32,7 +31,6 @@ describe('kuery functions', function () {
describe('not', function () {
-
beforeEach(() => {
indexPattern = indexPatternResponse;
});
@@ -56,6 +54,7 @@ describe('kuery functions', function () {
expect(result.bool).to.only.have.keys('must_not');
expect(result.bool.must_not).to.eql(ast.toElasticsearchQuery(childNode, indexPattern));
});
+
});
});
});
diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js b/packages/kbn-es-query/src/kuery/functions/__tests__/or.js
index 3b5bf27f2d8cb4..f24f24b98e7fbd 100644
--- a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js
+++ b/packages/kbn-es-query/src/kuery/functions/__tests__/or.js
@@ -23,7 +23,6 @@ import { nodeTypes } from '../../node_types';
import * as ast from '../../ast';
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
-
let indexPattern;
const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx');
@@ -33,11 +32,11 @@ describe('kuery functions', function () {
describe('or', function () {
-
beforeEach(() => {
indexPattern = indexPatternResponse;
});
+
describe('buildNodeParams', function () {
it('arguments should contain the unmodified child nodes', function () {
diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js b/packages/kbn-es-query/src/kuery/functions/__tests__/range.js
index a8c0b8157405d0..4f290206c8bfb7 100644
--- a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js
+++ b/packages/kbn-es-query/src/kuery/functions/__tests__/range.js
@@ -22,7 +22,6 @@ import * as range from '../range';
import { nodeTypes } from '../../node_types';
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
-
let indexPattern;
describe('kuery functions', function () {
@@ -136,6 +135,52 @@ describe('kuery functions', function () {
expect(result.bool.should[0]).to.have.key('script');
});
+ it('should support date fields without a dateFormat provided', function () {
+ const expected = {
+ bool: {
+ should: [
+ {
+ range: {
+ '@timestamp': {
+ gt: '2018-01-03T19:04:17',
+ lt: '2018-04-03T19:04:17',
+ }
+ }
+ }
+ ],
+ minimum_should_match: 1
+ }
+ };
+
+ const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' });
+ const result = range.toElasticsearchQuery(node, indexPattern);
+ expect(result).to.eql(expected);
+ });
+
+ it('should support date fields with a dateFormat provided', function () {
+ const config = { dateFormatTZ: 'America/Phoenix' };
+ const expected = {
+ bool: {
+ should: [
+ {
+ range: {
+ '@timestamp': {
+ gt: '2018-01-03T19:04:17',
+ lt: '2018-04-03T19:04:17',
+ time_zone: 'America/Phoenix',
+ }
+ }
+ }
+ ],
+ minimum_should_match: 1
+ }
+ };
+
+ const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' });
+ const result = range.toElasticsearchQuery(node, indexPattern, config);
+ expect(result).to.eql(expected);
+ });
+
});
});
});
diff --git a/packages/kbn-es-query/src/kuery/functions/and.js b/packages/kbn-es-query/src/kuery/functions/and.js
index a727af65f56ed5..68e125ea4de59f 100644
--- a/packages/kbn-es-query/src/kuery/functions/and.js
+++ b/packages/kbn-es-query/src/kuery/functions/and.js
@@ -25,13 +25,13 @@ export function buildNodeParams(children) {
};
}
-export function toElasticsearchQuery(node, indexPattern) {
+export function toElasticsearchQuery(node, indexPattern, config) {
const children = node.arguments || [];
return {
bool: {
filter: children.map((child) => {
- return ast.toElasticsearchQuery(child, indexPattern);
+ return ast.toElasticsearchQuery(child, indexPattern, config);
})
}
};
diff --git a/packages/kbn-es-query/src/kuery/functions/is.js b/packages/kbn-es-query/src/kuery/functions/is.js
index 27e64d23c1542e..0338671e9b3fe4 100644
--- a/packages/kbn-es-query/src/kuery/functions/is.js
+++ b/packages/kbn-es-query/src/kuery/functions/is.js
@@ -23,6 +23,7 @@ import * as literal from '../node_types/literal';
import * as wildcard from '../node_types/wildcard';
import { getPhraseScript } from '../../filters';
import { getFields } from './utils/get_fields';
+import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings';
export function buildNodeParams(fieldName, value, isPhrase = false) {
if (_.isUndefined(fieldName)) {
@@ -35,19 +36,16 @@ export function buildNodeParams(fieldName, value, isPhrase = false) {
const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName);
const valueNode = typeof value === 'string' ? ast.fromLiteralExpression(value) : literal.buildNode(value);
const isPhraseNode = literal.buildNode(isPhrase);
-
return {
arguments: [fieldNode, valueNode, isPhraseNode],
};
}
-export function toElasticsearchQuery(node, indexPattern) {
+export function toElasticsearchQuery(node, indexPattern = null, config = {}) {
const { arguments: [ fieldNameArg, valueArg, isPhraseArg ] } = node;
-
const fieldName = ast.toElasticsearchQuery(fieldNameArg);
const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg;
const type = isPhraseArg.value ? 'phrase' : 'best_fields';
-
if (fieldNameArg.value === null) {
if (valueArg.type === 'wildcard') {
return {
@@ -67,7 +65,6 @@ export function toElasticsearchQuery(node, indexPattern) {
}
const fields = indexPattern ? getFields(fieldNameArg, indexPattern) : [];
-
// If no fields are found in the index pattern we send through the given field name as-is. We do this to preserve
// the behaviour of lucene on dashboards where there are panels based on different index patterns that have different
// fields. If a user queries on a field that exists in one pattern but not the other, the index pattern without the
@@ -116,6 +113,22 @@ export function toElasticsearchQuery(node, indexPattern) {
}
}];
}
+ /*
+ If we detect that it's a date field and the user wants an exact date, we need to convert the query to both >= and <= the value provided to force a range query. This is because match and match_phrase queries do not accept a timezone parameter.
+ dateFormatTZ can have the value of 'Browser', in which case we guess the timezone using moment.tz.guess.
+ */
+ else if (field.type === 'date') {
+ const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {};
+ return [...accumulator, {
+ range: {
+ [field.name]: {
+ gte: value,
+ lte: value,
+ ...timeZoneParam,
+ },
+ }
+ }];
+ }
else {
const queryType = type === 'phrase' ? 'match_phrase' : 'match';
return [...accumulator, {
@@ -134,3 +147,4 @@ export function toElasticsearchQuery(node, indexPattern) {
};
}
+
diff --git a/packages/kbn-es-query/src/kuery/functions/not.js b/packages/kbn-es-query/src/kuery/functions/not.js
index 3f077660440838..d3ab14df16bcb4 100644
--- a/packages/kbn-es-query/src/kuery/functions/not.js
+++ b/packages/kbn-es-query/src/kuery/functions/not.js
@@ -25,12 +25,12 @@ export function buildNodeParams(child) {
};
}
-export function toElasticsearchQuery(node, indexPattern) {
+export function toElasticsearchQuery(node, indexPattern, config) {
const [ argument ] = node.arguments;
return {
bool: {
- must_not: ast.toElasticsearchQuery(argument, indexPattern)
+ must_not: ast.toElasticsearchQuery(argument, indexPattern, config)
}
};
}
diff --git a/packages/kbn-es-query/src/kuery/functions/or.js b/packages/kbn-es-query/src/kuery/functions/or.js
index ea834b8f5c5f76..918d46a6691de4 100644
--- a/packages/kbn-es-query/src/kuery/functions/or.js
+++ b/packages/kbn-es-query/src/kuery/functions/or.js
@@ -25,13 +25,13 @@ export function buildNodeParams(children) {
};
}
-export function toElasticsearchQuery(node, indexPattern) {
+export function toElasticsearchQuery(node, indexPattern, config) {
const children = node.arguments || [];
return {
bool: {
should: children.map((child) => {
- return ast.toElasticsearchQuery(child, indexPattern);
+ return ast.toElasticsearchQuery(child, indexPattern, config);
}),
minimum_should_match: 1,
},
diff --git a/packages/kbn-es-query/src/kuery/functions/range.js b/packages/kbn-es-query/src/kuery/functions/range.js
index 40c53e55c2011b..df77baf4b02083 100644
--- a/packages/kbn-es-query/src/kuery/functions/range.js
+++ b/packages/kbn-es-query/src/kuery/functions/range.js
@@ -22,6 +22,7 @@ import { nodeTypes } from '../node_types';
import * as ast from '../ast';
import { getRangeScript } from '../../filters';
import { getFields } from './utils/get_fields';
+import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings';
export function buildNodeParams(fieldName, params) {
params = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format');
@@ -35,7 +36,7 @@ export function buildNodeParams(fieldName, params) {
};
}
-export function toElasticsearchQuery(node, indexPattern) {
+export function toElasticsearchQuery(node, indexPattern = null, config = {}) {
const [ fieldNameArg, ...args ] = node.arguments;
const fields = indexPattern ? getFields(fieldNameArg, indexPattern) : [];
const namedArgs = extractArguments(args);
@@ -60,7 +61,17 @@ export function toElasticsearchQuery(node, indexPattern) {
script: getRangeScript(field, queryParams),
};
}
-
+ else if (field.type === 'date') {
+ const timeZoneParam = config.dateFormatTZ ? { time_zone: getTimeZoneFromSettings(config.dateFormatTZ) } : {};
+ return {
+ range: {
+ [field.name]: {
+ ...queryParams,
+ ...timeZoneParam,
+ }
+ }
+ };
+ }
return {
range: {
[field.name]: queryParams
diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js
index 2ccb3bd5991d85..de00c083fc8304 100644
--- a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js
+++ b/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js
@@ -31,7 +31,6 @@ describe('kuery node types', function () {
let indexPattern;
-
beforeEach(() => {
indexPattern = indexPatternResponse;
});
diff --git a/packages/kbn-es-query/src/kuery/node_types/function.js b/packages/kbn-es-query/src/kuery/node_types/function.js
index 7bcae9358565b2..6b10bb1f704c85 100644
--- a/packages/kbn-es-query/src/kuery/node_types/function.js
+++ b/packages/kbn-es-query/src/kuery/node_types/function.js
@@ -21,8 +21,8 @@ import _ from 'lodash';
import { functions } from '../functions';
export function buildNode(functionName, ...functionArgs) {
- const kueryFunction = functions[functionName];
+ const kueryFunction = functions[functionName];
if (_.isUndefined(kueryFunction)) {
throw new Error(`Unknown function "${functionName}"`);
}
@@ -47,8 +47,8 @@ export function buildNodeWithArgumentNodes(functionName, argumentNodes) {
};
}
-export function toElasticsearchQuery(node, indexPattern) {
+export function toElasticsearchQuery(node, indexPattern, config = {}) {
const kueryFunction = functions[node.function];
- return kueryFunction.toElasticsearchQuery(node, indexPattern);
+ return kueryFunction.toElasticsearchQuery(node, indexPattern, config);
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/retrieve_and_export_docs.js b/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js
similarity index 59%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/retrieve_and_export_docs.js
rename to packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js
index 25de287ebb8523..6deaccadfdb76c 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/retrieve_and_export_docs.js
+++ b/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js
@@ -17,19 +17,20 @@
* under the License.
*/
-import { saveToFile } from './';
+import expect from '@kbn/expect';
+import { getTimeZoneFromSettings } from '../get_time_zone_from_settings';
-export async function retrieveAndExportDocs(objs, savedObjectsClient) {
- const response = await savedObjectsClient.bulkGet(objs);
- const objects = response.savedObjects.map(obj => {
- return {
- _id: obj.id,
- _type: obj.type,
- _source: obj.attributes,
- _migrationVersion: obj.migrationVersion,
- _references: obj.references,
- };
+describe('get timezone from settings', function () {
+
+ it('should return the config timezone if the time zone is set', function () {
+ const result = getTimeZoneFromSettings('America/Chicago');
+ expect(result).to.eql('America/Chicago');
+ });
+
+ it('should return the system timezone if the time zone is set to "Browser"', function () {
+ const result = getTimeZoneFromSettings('Browser');
+ expect(result).to.not.equal('Browser');
});
- saveToFile(JSON.stringify(objects, null, 2));
-}
+});
+
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/scan_all_types.js b/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js
similarity index 74%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/scan_all_types.js
rename to packages/kbn-es-query/src/utils/get_time_zone_from_settings.js
index 116f6c58c3433d..1a06941ece1274 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/scan_all_types.js
+++ b/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js
@@ -17,10 +17,12 @@
* under the License.
*/
-import chrome from 'ui/chrome';
+import moment from 'moment-timezone';
+const detectedTimezone = moment.tz.guess();
-const apiBase = chrome.addBasePath('/api/kibana/management/saved_objects/scroll');
-export async function scanAllTypes($http, typesToInclude) {
- const results = await $http.post(`${apiBase}/export`, { typesToInclude });
- return results.data;
+export function getTimeZoneFromSettings(dateFormatTZ) {
+ if (dateFormatTZ === 'Browser') {
+ return detectedTimezone;
+ }
+ return dateFormatTZ;
}
diff --git a/src/legacy/ui/public/flyout/index.ts b/packages/kbn-es-query/src/utils/index.js
similarity index 94%
rename from src/legacy/ui/public/flyout/index.ts
rename to packages/kbn-es-query/src/utils/index.js
index 2c0f11bcc72ba4..27f51c1f44cf2f 100644
--- a/src/legacy/ui/public/flyout/index.ts
+++ b/packages/kbn-es-query/src/utils/index.js
@@ -17,4 +17,4 @@
* under the License.
*/
-export * from './flyout_session';
+export * from './get_time_zone_from_settings';
diff --git a/src/core/README.md b/src/core/README.md
index 196946ed9e4a31..352e159d4504f8 100644
--- a/src/core/README.md
+++ b/src/core/README.md
@@ -2,6 +2,12 @@
Core is a set of systems (frontend, backend etc.) that Kibana and its plugins are built on top of.
+## Plugin development
+Core Plugin API Documentation:
+ - [Core Public API](/docs/development/core/public/kibana-plugin-public.md)
+ - [Core Server API](/docs/development/core/server/kibana-plugin-server.md)
+ - [Migration guide for porting existing plugins](./MIGRATION.md)
+
## Integration with the "legacy" Kibana
Most of the existing core functionality is still spread over "legacy" Kibana and it will take some time to upgrade it.
@@ -19,3 +25,17 @@ by the "legacy" Kibana may be rejected by the `core` now.
Even though `core` has its own logging system it doesn't output log records directly (e.g. to file or terminal), but instead
forward them to the "legacy" Kibana so that they look the same as the rest of the log records throughout Kibana.
+
+## Core API Review
+To provide a stable API for plugin developers, it is important that the Core Public and Server API's are stable and
+well documented. To reduce the chance of regressions, development on the Core API's includes an API signature review
+process described below. Changes to the API signature which have not been accepted will cause the build to fail.
+
+When changes to the Core API's signatures are made, the following process needs to be followed:
+1. After changes have been made, run `yarn core:acceptApiChanges` which performs the following:
+ - Recompiles all typescript typings files
+ - Updates the API review files `src/core/public/kibana.api.md` and `src/core/server/kibana.api.md`
+ - Updates the Core API documentation in `docs/development/core/`
+2. Review and commit the updated API Review files and documentation
+3. Clearly flag any breaking changes in your pull request
+
diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts
index c7bb01a015cde3..27eafb2a4e9c9b 100644
--- a/src/core/public/core_system.test.mocks.ts
+++ b/src/core/public/core_system.test.mocks.ts
@@ -25,6 +25,7 @@ import { i18nServiceMock } from './i18n/i18n_service.mock';
import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock';
import { legacyPlatformServiceMock } from './legacy/legacy_service.mock';
import { notificationServiceMock } from './notifications/notifications_service.mock';
+import { overlayServiceMock } from './overlays/overlay_service.mock';
import { pluginsServiceMock } from './plugins/plugins_service.mock';
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
@@ -92,6 +93,12 @@ jest.doMock('./chrome', () => ({
ChromeService: ChromeServiceConstructor,
}));
+export const MockOverlayService = overlayServiceMock.create();
+export const OverlayServiceConstructor = jest.fn().mockImplementation(() => MockOverlayService);
+jest.doMock('./overlays', () => ({
+ OverlayService: OverlayServiceConstructor,
+}));
+
export const MockPluginsService = pluginsServiceMock.create();
export const PluginsServiceConstructor = jest.fn().mockImplementation(() => MockPluginsService);
jest.doMock('./plugins', () => ({
diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts
index 4551da13c24f1f..9a5840829453ee 100644
--- a/src/core/public/core_system.test.ts
+++ b/src/core/public/core_system.test.ts
@@ -36,9 +36,11 @@ import {
MockInjectedMetadataService,
MockLegacyPlatformService,
MockNotificationsService,
+ MockOverlayService,
MockPluginsService,
MockUiSettingsService,
NotificationServiceConstructor,
+ OverlayServiceConstructor,
UiSettingsServiceConstructor,
} from './core_system.test.mocks';
@@ -81,6 +83,7 @@ describe('constructor', () => {
expect(BasePathServiceConstructor).toHaveBeenCalledTimes(1);
expect(UiSettingsServiceConstructor).toHaveBeenCalledTimes(1);
expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1);
+ expect(OverlayServiceConstructor).toHaveBeenCalledTimes(1);
});
it('passes injectedMetadata param to InjectedMetadataService', () => {
@@ -229,7 +232,7 @@ describe('#setup()', () => {
const root = document.createElement('div');
root.innerHTML = 'foo bar
';
await setupCore(root);
- expect(root.innerHTML).toBe('
');
+ expect(root.innerHTML).toBe('
');
});
it('calls injectedMetadata#setup()', async () => {
@@ -272,6 +275,11 @@ describe('#setup()', () => {
expect(MockChromeService.setup).toHaveBeenCalledTimes(1);
});
+ it('calls overlays#setup()', () => {
+ setupCore();
+ expect(MockOverlayService.setup).toHaveBeenCalledTimes(1);
+ });
+
it('calls plugin#setup()', async () => {
await setupCore();
expect(MockPluginsService.setup).toHaveBeenCalledTimes(1);
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index 106c55c91e09c8..8d3bf01322342e 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -30,6 +30,7 @@ import { I18nService } from './i18n';
import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformParams, LegacyPlatformService } from './legacy';
import { NotificationsService } from './notifications';
+import { OverlayService } from './overlays';
import { PluginsService } from './plugins';
import { UiSettingsService } from './ui_settings';
@@ -63,11 +64,13 @@ export class CoreSystem {
private readonly basePath: BasePathService;
private readonly chrome: ChromeService;
private readonly i18n: I18nService;
+ private readonly overlay: OverlayService;
private readonly plugins: PluginsService;
private readonly rootDomElement: HTMLElement;
private readonly notificationsTargetDomElement$: Subject;
private readonly legacyPlatformTargetDomElement: HTMLDivElement;
+ private readonly overlayTargetDomElement: HTMLDivElement;
constructor(params: Params) {
const {
@@ -101,6 +104,8 @@ export class CoreSystem {
this.http = new HttpService();
this.basePath = new BasePathService();
this.uiSettings = new UiSettingsService();
+ this.overlayTargetDomElement = document.createElement('div');
+ this.overlay = new OverlayService(this.overlayTargetDomElement);
this.chrome = new ChromeService({ browserSupportsCsp });
const core: CoreContext = {};
@@ -121,6 +126,7 @@ export class CoreSystem {
const injectedMetadata = this.injectedMetadata.setup();
const fatalErrors = this.fatalErrors.setup({ i18n });
const http = this.http.setup({ fatalErrors });
+ const overlays = this.overlay.setup({ i18n });
const basePath = this.basePath.setup({ injectedMetadata });
const uiSettings = this.uiSettings.setup({
notifications,
@@ -142,6 +148,7 @@ export class CoreSystem {
injectedMetadata,
notifications,
uiSettings,
+ overlays,
};
await this.plugins.setup(core);
@@ -153,6 +160,7 @@ export class CoreSystem {
const notificationsTargetDomElement = document.createElement('div');
this.rootDomElement.appendChild(notificationsTargetDomElement);
this.rootDomElement.appendChild(this.legacyPlatformTargetDomElement);
+ this.rootDomElement.appendChild(this.overlayTargetDomElement);
// Only provide the DOM element to notifications once it's attached to the page.
// This prevents notifications from timing out before being displayed.
diff --git a/src/core/public/i18n/i18n_service.mock.ts b/src/core/public/i18n/i18n_service.mock.ts
index 26356d7795f731..af6eb0f2f8ff99 100644
--- a/src/core/public/i18n/i18n_service.mock.ts
+++ b/src/core/public/i18n/i18n_service.mock.ts
@@ -16,11 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
+
+import React from 'react';
import { I18nService, I18nSetup } from './i18n_service';
+const PassThroughComponent = ({ children }: { children: React.ReactNode }) => children;
+
const createSetupContractMock = () => {
const setupContract: jest.Mocked = {
- Context: jest.fn(),
+ // By default mock the Context component so it simply renders all children
+ Context: jest.fn().mockImplementation(PassThroughComponent),
};
return setupContract;
};
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 7ef62c517d7ea6..4aa0b533cc3956 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -24,6 +24,7 @@ import { HttpSetup } from './http';
import { I18nSetup } from './i18n';
import { InjectedMetadataParams, InjectedMetadataSetup } from './injected_metadata';
import { NotificationsSetup, Toast, ToastInput, ToastsSetup } from './notifications';
+import { FlyoutRef, OverlaySetup } from './overlays';
import { Plugin, PluginInitializer, PluginInitializerContext, PluginSetupContext } from './plugins';
import { UiSettingsClient, UiSettingsSetup, UiSettingsState } from './ui_settings';
@@ -43,6 +44,7 @@ export interface CoreSetup {
basePath: BasePathSetup;
uiSettings: UiSettingsSetup;
chrome: ChromeSetup;
+ overlays: OverlaySetup;
}
export {
@@ -61,6 +63,8 @@ export {
PluginInitializerContext,
PluginSetupContext,
NotificationsSetup,
+ OverlaySetup,
+ FlyoutRef,
Toast,
ToastInput,
ToastsSetup,
diff --git a/src/core/public/kibana.api.md b/src/core/public/kibana.api.md
index a9615cc935a044..ae9ca480ed4507 100644
--- a/src/core/public/kibana.api.md
+++ b/src/core/public/kibana.api.md
@@ -4,8 +4,10 @@
```ts
+import * as CSS from 'csstype';
import { default } from 'react';
import { Observable } from 'rxjs';
+import * as PropTypes from 'prop-types';
import * as Rx from 'rxjs';
import { Toast } from '@elastic/eui';
@@ -63,6 +65,8 @@ export interface CoreSetup {
// (undocumented)
notifications: NotificationsSetup;
// (undocumented)
+ overlays: OverlaySetup;
+ // (undocumented)
uiSettings: UiSettingsSetup;
}
@@ -93,6 +97,14 @@ export class CoreSystem {
// @public (undocumented)
export type FatalErrorsSetup = ReturnType;
+// @public
+export class FlyoutRef {
+ // (undocumented)
+ constructor();
+ close(): Promise;
+ readonly onClose: Promise;
+}
+
// Warning: (ae-forgotten-export) The symbol "HttpService" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
@@ -152,6 +164,17 @@ export type InjectedMetadataSetup = ReturnType
// @public (undocumented)
export type NotificationsSetup = ReturnType;
+// @public (undocumented)
+export interface OverlaySetup {
+ // Warning: (ae-forgotten-export) The symbol "React" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
+ openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
+ closeButtonAriaLabel?: string;
+ 'data-test-subj'?: string;
+ }) => FlyoutRef;
+}
+
// @public
export interface Plugin = {}> {
// (undocumented)
diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts
index 1328a328642c06..1aadf271f5e549 100644
--- a/src/core/public/legacy/legacy_service.test.ts
+++ b/src/core/public/legacy/legacy_service.test.ts
@@ -148,6 +148,7 @@ import { httpServiceMock } from '../http/http_service.mock';
import { i18nServiceMock } from '../i18n/i18n_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { notificationServiceMock } from '../notifications/notifications_service.mock';
+import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { LegacyPlatformService } from './legacy_service';
@@ -159,6 +160,7 @@ const i18nSetup = i18nServiceMock.createSetupContract();
const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract();
const notificationsSetup = notificationServiceMock.createSetupContract();
const uiSettingsSetup = uiSettingsServiceMock.createSetupContract();
+const overlaySetup = overlayServiceMock.createSetupContract();
const defaultParams = {
targetDomElement: document.createElement('div'),
@@ -176,6 +178,7 @@ const defaultSetupDeps = {
basePath: basePathSetup,
uiSettings: uiSettingsSetup,
chrome: chromeSetup,
+ overlays: overlaySetup,
};
afterEach(() => {
diff --git a/src/core/public/overlays/__snapshots__/flyout.test.tsx.snap b/src/core/public/overlays/__snapshots__/flyout.test.tsx.snap
new file mode 100644
index 00000000000000..0c19c6312a6726
--- /dev/null
+++ b/src/core/public/overlays/__snapshots__/flyout.test.tsx.snap
@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FlyoutService FlyoutRef#close() can be called multiple times on the same FlyoutRef 1`] = `
+Array [
+ Array [
+
,
+ ],
+]
+`;
+
+exports[`FlyoutService openFlyout() renders a flyout to the DOM 1`] = `
+Array [
+ Array [
+
+
+
+ Flyout content
+
+
+ ,
+
,
+ ],
+]
+`;
+
+exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
+Array [
+ Array [
+
+
+
+ Flyout content 1
+
+
+ ,
+
,
+ ],
+ Array [
+
+
+
+ Flyout content 2
+
+
+ ,
+
,
+ ],
+]
+`;
diff --git a/src/core/public/overlays/flyout.test.mocks.ts b/src/core/public/overlays/flyout.test.mocks.ts
new file mode 100644
index 00000000000000..35a046e9600774
--- /dev/null
+++ b/src/core/public/overlays/flyout.test.mocks.ts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const mockReactDomRender = jest.fn();
+export const mockReactDomUnmount = jest.fn();
+jest.doMock('react-dom', () => ({
+ render: mockReactDomRender,
+ unmountComponentAtNode: mockReactDomUnmount,
+}));
diff --git a/src/core/public/overlays/flyout.test.tsx b/src/core/public/overlays/flyout.test.tsx
new file mode 100644
index 00000000000000..0245fb46195e92
--- /dev/null
+++ b/src/core/public/overlays/flyout.test.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 { mockReactDomRender, mockReactDomUnmount } from './flyout.test.mocks';
+
+import React from 'react';
+import { i18nServiceMock } from '../i18n/i18n_service.mock';
+import { FlyoutRef, FlyoutService } from './flyout';
+
+const i18nMock = i18nServiceMock.createSetupContract();
+
+beforeEach(() => {
+ mockReactDomRender.mockClear();
+ mockReactDomUnmount.mockClear();
+});
+
+describe('FlyoutService', () => {
+ describe('openFlyout()', () => {
+ it('renders a flyout to the DOM', () => {
+ const target = document.createElement('div');
+ const flyoutService = new FlyoutService(target);
+ expect(mockReactDomRender).not.toHaveBeenCalled();
+ flyoutService.openFlyout(i18nMock, Flyout content );
+ expect(mockReactDomRender.mock.calls).toMatchSnapshot();
+ });
+ describe('with a currently active flyout', () => {
+ let target: HTMLElement, flyoutService: FlyoutService, ref1: FlyoutRef;
+ beforeEach(() => {
+ target = document.createElement('div');
+ flyoutService = new FlyoutService(target);
+ ref1 = flyoutService.openFlyout(i18nMock, Flyout content 1 );
+ });
+ it('replaces the current flyout with a new one', () => {
+ flyoutService.openFlyout(i18nMock, Flyout content 2 );
+ expect(mockReactDomRender.mock.calls).toMatchSnapshot();
+ expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
+ expect(() => ref1.close()).not.toThrowError();
+ expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
+ });
+ it('resolves onClose on the previous ref', async () => {
+ const onCloseComplete = jest.fn();
+ ref1.onClose.then(onCloseComplete);
+ flyoutService.openFlyout(i18nMock, Flyout content 2 );
+ await ref1.onClose;
+ expect(onCloseComplete).toBeCalledTimes(1);
+ });
+ });
+ });
+ describe('FlyoutRef#close()', () => {
+ it('resolves the onClose Promise', async () => {
+ const target = document.createElement('div');
+ const flyoutService = new FlyoutService(target);
+ const ref = flyoutService.openFlyout(i18nMock, Flyout content );
+
+ const onCloseComplete = jest.fn();
+ ref.onClose.then(onCloseComplete);
+ await ref.close();
+ await ref.close();
+ expect(onCloseComplete).toHaveBeenCalledTimes(1);
+ });
+ it('can be called multiple times on the same FlyoutRef', async () => {
+ const target = document.createElement('div');
+ const flyoutService = new FlyoutService(target);
+ const ref = flyoutService.openFlyout(i18nMock, Flyout content );
+ expect(mockReactDomUnmount).not.toHaveBeenCalled();
+ await ref.close();
+ expect(mockReactDomUnmount.mock.calls).toMatchSnapshot();
+ await ref.close();
+ expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
+ });
+ it("on a stale FlyoutRef doesn't affect the active flyout", async () => {
+ const target = document.createElement('div');
+ const flyoutService = new FlyoutService(target);
+ const ref1 = flyoutService.openFlyout(i18nMock, Flyout content 1 );
+ const ref2 = flyoutService.openFlyout(i18nMock, Flyout content 2 );
+ const onCloseComplete = jest.fn();
+ ref2.onClose.then(onCloseComplete);
+ mockReactDomUnmount.mockClear();
+ await ref1.close();
+ expect(mockReactDomUnmount).toBeCalledTimes(0);
+ expect(onCloseComplete).toBeCalledTimes(0);
+ });
+ });
+});
diff --git a/src/core/public/overlays/flyout.tsx b/src/core/public/overlays/flyout.tsx
new file mode 100644
index 00000000000000..c5b68cb19b2b85
--- /dev/null
+++ b/src/core/public/overlays/flyout.tsx
@@ -0,0 +1,130 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* eslint-disable max-classes-per-file */
+
+import { EuiFlyout } from '@elastic/eui';
+import React from 'react';
+import { render, unmountComponentAtNode } from 'react-dom';
+import { Subject } from 'rxjs';
+import { I18nSetup } from '../i18n';
+
+/**
+ * A FlyoutRef is a reference to an opened flyout panel. It offers methods to
+ * close the flyout panel again. If you open a flyout panel you should make
+ * sure you call `close()` when it should be closed.
+ * Since a flyout could also be closed by a user or from another flyout being
+ * opened, you must bind to the `onClose` Promise on the FlyoutRef instance.
+ * The Promise will resolve whenever the flyout was closed at which point you
+ * should discard the FlyoutRef.
+ *
+ * @public
+ */
+export class FlyoutRef {
+ /**
+ * An Promise that will resolve once this flyout is closed.
+ *
+ * Flyouts can close from user interaction, calling `close()` on the flyout
+ * reference or another call to `openFlyout()` replacing your flyout.
+ */
+ public readonly onClose: Promise;
+
+ private closeSubject = new Subject();
+
+ constructor() {
+ this.onClose = this.closeSubject.toPromise();
+ }
+
+ /**
+ * Closes the referenced flyout if it's still open which in turn will
+ * resolve the `onClose` Promise. If the flyout had already been
+ * closed this method does nothing.
+ */
+ public close(): Promise {
+ if (!this.closeSubject.closed) {
+ this.closeSubject.next();
+ this.closeSubject.complete();
+ }
+ return this.onClose;
+ }
+}
+
+/** @internal */
+export class FlyoutService {
+ private activeFlyout: FlyoutRef | null = null;
+
+ constructor(private readonly targetDomElement: Element) {}
+
+ /**
+ * Opens a flyout panel with the given component inside. You can use
+ * `close()` on the returned FlyoutRef to close the flyout.
+ *
+ * @param flyoutChildren - Mounts the children inside a flyout panel
+ * @return {FlyoutRef} A reference to the opened flyout panel.
+ */
+ public openFlyout = (
+ i18n: I18nSetup,
+ flyoutChildren: React.ReactNode,
+ flyoutProps: {
+ closeButtonAriaLabel?: string;
+ 'data-test-subj'?: string;
+ } = {}
+ ): FlyoutRef => {
+ // If there is an active flyout session close it before opening a new one.
+ if (this.activeFlyout) {
+ this.activeFlyout.close();
+ this.cleanupDom();
+ }
+
+ const flyout = new FlyoutRef();
+
+ // If a flyout gets closed through it's FlyoutRef, remove it from the dom
+ flyout.onClose.then(() => {
+ if (this.activeFlyout === flyout) {
+ this.cleanupDom();
+ }
+ });
+
+ this.activeFlyout = flyout;
+
+ render(
+
+ flyout.close()}>
+ {flyoutChildren}
+
+ ,
+ this.targetDomElement
+ );
+
+ return flyout;
+ };
+
+ /**
+ * Using React.Render to re-render into a target DOM element will replace
+ * the content of the target but won't call unmountComponent on any
+ * components inside the target or any of their children. So we properly
+ * cleanup the DOM here to prevent subtle bugs in child components which
+ * depend on unmounting for cleanup behaviour.
+ */
+ private cleanupDom(): void {
+ unmountComponentAtNode(this.targetDomElement);
+ this.targetDomElement.innerHTML = '';
+ this.activeFlyout = null;
+ }
+}
diff --git a/src/core/public/overlays/index.ts b/src/core/public/overlays/index.ts
new file mode 100644
index 00000000000000..117868379570c6
--- /dev/null
+++ b/src/core/public/overlays/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { OverlayService, OverlaySetup } from './overlay_service';
+export { FlyoutRef } from './flyout';
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/scan_all_types.test.js b/src/core/public/overlays/overlay_service.mock.ts
similarity index 59%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/scan_all_types.test.js
rename to src/core/public/overlays/overlay_service.mock.ts
index 2bfb004363c32a..3b21bfea1aff0c 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/scan_all_types.test.js
+++ b/src/core/public/overlays/overlay_service.mock.ts
@@ -16,21 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { OverlayService, OverlaySetup } from './overlay_service';
-import { scanAllTypes } from '../scan_all_types';
+const createSetupContractMock = () => {
+ const setupContract: jest.Mocked> = {
+ openFlyout: jest.fn(),
+ };
+ return setupContract;
+};
-jest.mock('ui/chrome', () => ({
- addBasePath: () => 'apiUrl',
-}));
+const createMock = () => {
+ const mocked: jest.Mocked> = {
+ setup: jest.fn(),
+ };
+ mocked.setup.mockReturnValue(createSetupContractMock());
+ return mocked;
+};
-describe('scanAllTypes', () => {
- it('should call the api', async () => {
- const $http = {
- post: jest.fn().mockImplementation(() => ([]))
- };
- const typesToInclude = ['index-pattern', 'dashboard'];
-
- await scanAllTypes($http, typesToInclude);
- expect($http.post).toBeCalledWith('apiUrl/export', { typesToInclude });
- });
-});
+export const overlayServiceMock = {
+ create: createMock,
+ createSetupContract: createSetupContractMock,
+};
diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts
new file mode 100644
index 00000000000000..9c03c96228c52a
--- /dev/null
+++ b/src/core/public/overlays/overlay_service.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FlyoutService } from './flyout';
+
+import { FlyoutRef } from '..';
+import { I18nSetup } from '../i18n';
+
+interface Deps {
+ i18n: I18nSetup;
+}
+
+/** @internal */
+export class OverlayService {
+ private flyoutService: FlyoutService;
+
+ constructor(targetDomElement: HTMLElement) {
+ this.flyoutService = new FlyoutService(targetDomElement);
+ }
+
+ public setup({ i18n }: Deps): OverlaySetup {
+ return {
+ openFlyout: this.flyoutService.openFlyout.bind(this.flyoutService, i18n),
+ };
+ }
+}
+
+/** @public */
+export interface OverlaySetup {
+ openFlyout: (
+ flyoutChildren: React.ReactNode,
+ flyoutProps?: {
+ closeButtonAriaLabel?: string;
+ 'data-test-subj'?: string;
+ }
+ ) => FlyoutRef;
+}
diff --git a/src/dev/build/lib/fs.js b/src/dev/build/lib/fs.js
index e554d8c8a0b27d..b8523e391a7433 100644
--- a/src/dev/build/lib/fs.js
+++ b/src/dev/build/lib/fs.js
@@ -17,9 +17,10 @@
* under the License.
*/
+import archiver from 'archiver';
import fs from 'fs';
import { createHash } from 'crypto';
-import { resolve, dirname, isAbsolute } from 'path';
+import { resolve, dirname, isAbsolute, sep } from 'path';
import { createGunzip } from 'zlib';
import { inspect } from 'util';
@@ -194,3 +195,15 @@ export async function untar(source, destination, extractOptions = {}) {
}),
]);
}
+
+export async function compress(type, options = {}, source, destination) {
+ const output = fs.createWriteStream(destination);
+ const archive = archiver(type, options);
+ const name = source.split(sep).slice(-1)[0];
+
+ archive.pipe(output);
+
+ return archive
+ .directory(source, name)
+ .finalize();
+}
diff --git a/src/dev/build/lib/index.js b/src/dev/build/lib/index.js
index 4481a29b360b9b..dd8794e6967b5c 100644
--- a/src/dev/build/lib/index.js
+++ b/src/dev/build/lib/index.js
@@ -31,6 +31,7 @@ export {
untar,
deleteAll,
deleteEmptyFolders,
+ compress,
} from './fs';
export { scanDelete } from './scan_delete';
export { scanCopy } from './scan_copy';
diff --git a/src/dev/build/tasks/create_archives_task.js b/src/dev/build/tasks/create_archives_task.js
index e23aadba588043..ea7d16d6081154 100644
--- a/src/dev/build/tasks/create_archives_task.js
+++ b/src/dev/build/tasks/create_archives_task.js
@@ -18,20 +18,7 @@
*/
import path from 'path';
-import { createWriteStream } from 'fs';
-import archiver from 'archiver';
-
-import { mkdirp } from '../lib';
-
-function compress(type, options = {}, source, destination) {
- const output = createWriteStream(destination);
- const archive = archiver(type, options);
- const name = source.split(path.sep).slice(-1)[0];
-
- archive.pipe(output);
-
- return archive.directory(source, name).finalize();
-}
+import { mkdirp, compress } from '../lib';
export const CreateArchivesTask = {
description: 'Creating the archives for each platform',
diff --git a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js
new file mode 100644
index 00000000000000..f4f2745e9671e1
--- /dev/null
+++ b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.js
@@ -0,0 +1,70 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { resolve } from 'path';
+import { compress, copyAll, mkdirp, write } from '../../../lib';
+import { dockerfileTemplate } from './templates';
+
+export async function bundleDockerFiles(config, log, build, scope) {
+ log.info(`Generating kibana${ scope.imageFlavor } docker build context bundle`);
+
+ const dockerFilesDirName = `kibana${ scope.imageFlavor }-${ scope.versionTag }-docker-build-context`;
+ const dockerFilesBuildDir = resolve(scope.dockerBuildDir, dockerFilesDirName);
+ const dockerFilesOutputDir = config.resolveFromTarget(
+ `${ dockerFilesDirName }.tar.gz`
+ );
+
+ // Create dockerfiles dir inside docker build dir
+ await mkdirp(dockerFilesBuildDir);
+
+ // Create a release Dockerfile
+ await write(
+ resolve(dockerFilesBuildDir, dockerfileTemplate.name),
+ dockerfileTemplate.generator({
+ ...scope,
+ usePublicArtifact: true
+ })
+ );
+
+ // Move relevant docker build files inside
+ // dockerfiles folder
+ await copyAll(
+ resolve(scope.dockerBuildDir, 'bin'),
+ resolve(dockerFilesBuildDir, 'bin'),
+ );
+ await copyAll(
+ resolve(scope.dockerBuildDir, 'config'),
+ resolve(dockerFilesBuildDir, 'config'),
+ );
+
+ // Compress dockerfiles dir created inside
+ // docker build dir as output it as a target
+ // on targets folder
+ await compress(
+ 'tar',
+ {
+ gzip: true,
+ gzipOptions: {
+ level: 9
+ }
+ },
+ dockerFilesBuildDir,
+ dockerFilesOutputDir
+ );
+}
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
index dceb98736c83d7..7c94ef046c2070 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
@@ -1,4 +1,7 @@
#!/bin/bash
+#
+# ** THIS IS AN AUTO-GENERATED FILE **
+#
# Run Kibana, using environment variables to set longopts defining Kibana's
# configuration.
diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.js b/src/dev/build/tasks/os_packages/docker_generator/run.js
index 85cafad0b0ecad..8a7d125dde4001 100644
--- a/src/dev/build/tasks/os_packages/docker_generator/run.js
+++ b/src/dev/build/tasks/os_packages/docker_generator/run.js
@@ -22,6 +22,7 @@ import { resolve } from 'path';
import { promisify } from 'util';
import { write, copyAll, mkdirp, exec } from '../../../lib';
import * as dockerTemplates from './templates';
+import { bundleDockerFiles } from './bundle_dockerfiles';
const accessAsync = promisify(access);
const linkAsync = promisify(link);
@@ -37,6 +38,16 @@ export async function runDockerGenerator(config, log, build) {
const artifactsDir = config.resolveFromTarget('.');
const dockerBuildDir = config.resolveFromRepo('build', 'kibana-docker', build.isOss() ? 'oss' : 'default');
const dockerOutputDir = config.resolveFromTarget(`kibana${ imageFlavor }-${ versionTag }-docker.tar.gz`);
+ const scope = {
+ artifactTarball,
+ imageFlavor,
+ versionTag,
+ license,
+ artifactsDir,
+ imageTag,
+ dockerBuildDir,
+ dockerOutputDir
+ };
// Verify if we have the needed kibana target in order
// to build the kibana docker image.
@@ -65,16 +76,6 @@ export async function runDockerGenerator(config, log, build) {
// Write all the needed docker config files
// into kibana-docker folder
- const scope = {
- artifactTarball,
- imageFlavor,
- versionTag,
- license,
- artifactsDir,
- imageTag,
- dockerOutputDir
- };
-
for (const [, dockerTemplate] of Object.entries(dockerTemplates)) {
await write(resolve(dockerBuildDir, dockerTemplate.name), dockerTemplate.generator(scope));
}
@@ -96,4 +97,7 @@ export async function runDockerGenerator(config, log, build) {
cwd: dockerBuildDir,
level: 'info',
});
+
+ // Pack Dockerfiles and create a target for them
+ await bundleDockerFiles(config, log, build, scope);
}
diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js
index 4cbdca66d16e3b..95b0740ca738ce 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js
+++ b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js
@@ -19,7 +19,15 @@
import dedent from 'dedent';
-function generator({ artifactTarball, versionTag, license }) {
+function generator({ artifactTarball, versionTag, license, usePublicArtifact }) {
+ const copyArtifactTarballInsideDockerOptFolder = () => {
+ if (usePublicArtifact) {
+ return `RUN cd /opt && curl --retry 8 -s -L -O https://artifacts.elastic.co/downloads/kibana/${ artifactTarball } && cd -`;
+ }
+
+ return `COPY ${ artifactTarball } /opt`;
+ };
+
return dedent(`
#
# ** THIS IS AN AUTO-GENERATED FILE **
@@ -30,7 +38,7 @@ function generator({ artifactTarball, versionTag, license }) {
# Extract Kibana and make various file manipulations.
################################################################################
FROM centos:7 AS prep_files
- COPY ${ artifactTarball } /opt
+ ${copyArtifactTarballInsideDockerOptFolder()}
RUN mkdir /usr/share/kibana
WORKDIR /usr/share/kibana
RUN tar --strip-components=1 -zxf /opt/${ artifactTarball }
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx
index 01f6da6421109a..0a995310ae0e52 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx
+++ b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx
@@ -17,11 +17,9 @@
* under the License.
*/
-import React from 'react';
-
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-
+import React from 'react';
import { ContextMenuAction } from 'ui/embeddable';
import { Inspector } from 'ui/inspector';
@@ -78,7 +76,7 @@ export function getInspectorPanelAction({
}
};
// In case the inspector gets closed (otherwise), restore the original destroy function
- session.on('closed', () => {
+ session.onClose.finally(() => {
embeddable.destroy = originalDestroy;
});
},
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap
index c57871546683d5..7949f862ca97e8 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap
@@ -77,76 +77,129 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
`;
exports[`ObjectsTable export should allow the user to choose when exporting all 1`] = `
-
- }
- confirmButtonText={
-
- }
- defaultFocusedButton="confirm"
- onCancel={[Function]}
- onConfirm={[Function]}
- title={
-
- }
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ name="includeReferencesDeep"
+ onChange={[Function]}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
exports[`ObjectsTable import should show the flyout 1`] = `
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js
index 7cda53d9252e01..9a260c6534f825 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js
@@ -24,6 +24,8 @@ import { ObjectsTable, INCLUDED_TYPES } from '../objects_table';
import { Flyout } from '../components/flyout/';
import { Relationships } from '../components/relationships/';
+jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
+
jest.mock('../components/header', () => ({
Header: () => 'Header',
}));
@@ -45,12 +47,12 @@ jest.mock('ui/chrome', () => ({
addBasePath: () => ''
}));
-jest.mock('../../../lib/retrieve_and_export_docs', () => ({
- retrieveAndExportDocs: jest.fn(),
+jest.mock('../../../lib/fetch_export_objects', () => ({
+ fetchExportObjects: jest.fn(),
}));
-jest.mock('../../../lib/scan_all_types', () => ({
- scanAllTypes: jest.fn(),
+jest.mock('../../../lib/fetch_export_by_type', () => ({
+ fetchExportByType: jest.fn(),
}));
jest.mock('../../../lib/get_saved_object_counts', () => ({
@@ -64,8 +66,8 @@ jest.mock('../../../lib/get_saved_object_counts', () => ({
})
}));
-jest.mock('../../../lib/save_to_file', () => ({
- saveToFile: jest.fn(),
+jest.mock('@elastic/filesaver', () => ({
+ saveAs: jest.fn(),
}));
jest.mock('../../../lib/get_relationships', () => ({
@@ -147,6 +149,7 @@ const defaultProps = {
};
let addDangerMock;
+let addSuccessMock;
describe('ObjectsTable', () => {
beforeEach(() => {
@@ -159,8 +162,10 @@ describe('ObjectsTable', () => {
return debounced;
};
addDangerMock = jest.fn();
+ addSuccessMock = jest.fn();
require('ui/notify').toastNotifications = {
addDanger: addDangerMock,
+ addSuccess: addSuccessMock,
};
});
@@ -222,7 +227,7 @@ describe('ObjectsTable', () => {
}))
};
- const { retrieveAndExportDocs } = require('../../../lib/retrieve_and_export_docs');
+ const { fetchExportObjects } = require('../../../lib/fetch_export_objects');
const component = shallowWithIntl(
{
// Set some as selected
component.instance().onSelectionChanged(mockSelectedSavedObjects);
- await component.instance().onExport();
+ await component.instance().onExport(true);
- expect(mockSavedObjectsClient.bulkGet).toHaveBeenCalledWith(mockSelectedSavedObjects);
- expect(retrieveAndExportDocs).toHaveBeenCalledWith(mockSavedObjects, mockSavedObjectsClient);
+ expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true);
+ expect(addSuccessMock).toHaveBeenCalledWith({ title: 'Your file is downloading in the background' });
});
it('should allow the user to choose when exporting all', async () => {
@@ -260,12 +265,12 @@ describe('ObjectsTable', () => {
component.find('Header').prop('onExportAll')();
component.update();
- expect(component.find('EuiConfirmModal')).toMatchSnapshot();
+ expect(component.find('EuiModal')).toMatchSnapshot();
});
it('should export all', async () => {
- const { scanAllTypes } = require('../../../lib/scan_all_types');
- const { saveToFile } = require('../../../lib/save_to_file');
+ const { fetchExportByType } = require('../../../lib/fetch_export_by_type');
+ const { saveAs } = require('@elastic/filesaver');
const component = shallowWithIntl(
{
component.update();
// Set up mocks
- scanAllTypes.mockImplementation(() => allSavedObjects);
+ const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' });
+ fetchExportByType.mockImplementation(() => blob);
await component.instance().onExportAll();
- expect(scanAllTypes).toHaveBeenCalledWith(defaultProps.$http, INCLUDED_TYPES);
- expect(saveToFile).toHaveBeenCalledWith(JSON.stringify(allSavedObjects, null, 2));
+ expect(fetchExportByType).toHaveBeenCalledWith(INCLUDED_TYPES, true);
+ expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson');
+ expect(addSuccessMock).toHaveBeenCalledWith({ title: 'Your file is downloading in the background' });
});
});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap
index 99a42cbddf8392..a87d9fdb4bd461 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap
@@ -22,43 +22,342 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
/>
-
+
+
+ }
+ >
+
+
+
+ ,
+ }
+ }
+ />
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Flyout conflicts should allow conflict resolution 2`] = `
+[MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "getConflictResolutions": [Function],
+ "state": Object {
+ "conflictedIndexPatterns": undefined,
+ "conflictedSavedObjectsLinkedToSavedSearches": undefined,
+ "conflictedSearchDocs": undefined,
+ "conflictingRecord": undefined,
+ "error": undefined,
+ "failedImports": Array [
+ Object {
+ "error": Object {
+ "references": Array [
+ Object {
+ "id": "MyIndexPattern*",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "missing_references",
+ },
+ "obj": Object {
+ "id": "1",
+ "title": "My Visualization",
+ "type": "visualization",
+ },
+ },
+ ],
+ "file": Object {
+ "name": "foo.ndjson",
+ "path": "/home/foo.ndjson",
+ },
+ "importCount": 0,
+ "indexPatterns": Array [
+ Object {
+ "id": "1",
+ },
+ Object {
+ "id": "2",
+ },
+ ],
+ "isLegacyFile": false,
+ "isOverwriteAllChecked": true,
+ "loadingMessage": undefined,
+ "status": "loading",
+ "unmatchedReferences": Array [
+ Object {
+ "existingIndexPatternId": "MyIndexPattern*",
+ "list": Array [
+ Object {
+ "id": "1",
+ "title": "My Visualization",
+ "type": "visualization",
+ },
+ ],
+ "newIndexPatternId": "2",
+ },
+ ],
+ },
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Object {
+ "failedImports": Array [],
+ "importCount": 1,
+ "status": "success",
+ },
+ },
+ ],
+}
+`;
+
+exports[`Flyout conflicts should handle errors 1`] = `
+
+ }
+>
+
+
+
+
+
+`;
+
+exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
+
+
+
+
- }
- >
-
-
-
- ,
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+ ,
+ }
}
- }
- />
-
-
+ />
+
+
+
`;
-exports[`Flyout conflicts should handle errors 1`] = `
+exports[`Flyout legacy conflicts should handle errors 1`] = `
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js
index 15ba70fa93b84c..d5b84c3589c671 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/flyout.test.js
@@ -22,6 +22,8 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { Flyout } from '../flyout';
+jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
+
jest.mock('ui/errors', () => ({
SavedObjectNotFound: class SavedObjectNotFound extends Error {
constructor(options) {
@@ -35,12 +37,20 @@ jest.mock('ui/errors', () => ({
},
}));
+jest.mock('../../../../../lib/import_file', () => ({
+ importFile: jest.fn(),
+}));
+
+jest.mock('../../../../../lib/resolve_import_errors', () => ({
+ resolveImportErrors: jest.fn(),
+}));
+
jest.mock('ui/chrome', () => ({
addBasePath: () => {},
}));
-jest.mock('../../../../../lib/import_file', () => ({
- importFile: jest.fn(),
+jest.mock('../../../../../lib/import_legacy_file', () => ({
+ importLegacyFile: jest.fn(),
}));
jest.mock('../../../../../lib/resolve_saved_objects', () => ({
@@ -57,13 +67,19 @@ const defaultProps = {
done: jest.fn(),
services: [],
newIndexPatternUrl: '',
+ getConflictResolutions: jest.fn(),
indexPatterns: {
getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]),
},
};
const mockFile = {
- path: '/home/foo.txt',
+ name: 'foo.ndjson',
+ path: '/home/foo.ndjson',
+};
+const legacyMockFile = {
+ name: 'foo.json',
+ path: '/home/foo.json',
};
describe('Flyout', () => {
@@ -104,8 +120,23 @@ describe('Flyout', () => {
expect(component.state('file')).toBe(mockFile);
});
+ it('should allow removing a file', async () => {
+ const component = shallowWithIntl( );
+
+ // Ensure all promises resolve
+ await Promise.resolve();
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component.state('file')).toBe(undefined);
+ component.find('EuiFilePicker').simulate('change', [mockFile]);
+ expect(component.state('file')).toBe(mockFile);
+ component.find('EuiFilePicker').simulate('change', []);
+ expect(component.state('file')).toBe(undefined);
+ });
+
it('should handle invalid files', async () => {
- const { importFile } = require('../../../../../lib/import_file');
+ const { importLegacyFile } = require('../../../../../lib/import_legacy_file');
const component = shallowWithIntl( );
// Ensure all promises resolve
@@ -113,18 +144,18 @@ describe('Flyout', () => {
// Ensure the state changes are reflected
component.update();
- importFile.mockImplementation(() => {
+ importLegacyFile.mockImplementation(() => {
throw new Error('foobar');
});
- await component.instance().import();
+ await component.instance().legacyImport();
expect(component.state('error')).toBe('The file could not be processed.');
- importFile.mockImplementation(() => ({
+ importLegacyFile.mockImplementation(() => ({
invalid: true,
}));
- await component.instance().import();
+ await component.instance().legacyImport();
expect(component.state('error')).toBe(
'Saved objects file format is invalid and cannot be imported.'
);
@@ -132,6 +163,157 @@ describe('Flyout', () => {
describe('conflicts', () => {
const { importFile } = require('../../../../../lib/import_file');
+ const { resolveImportErrors } = require('../../../../../lib/resolve_import_errors');
+
+ beforeEach(() => {
+ importFile.mockImplementation(() => ({
+ success: false,
+ successCount: 0,
+ errors: [
+ {
+ id: '1',
+ type: 'visualization',
+ title: 'My Visualization',
+ error: {
+ type: 'missing_references',
+ references: [
+ {
+ id: 'MyIndexPattern*',
+ type: 'index-pattern',
+ },
+ ],
+ }
+ },
+ ],
+ }));
+ resolveImportErrors.mockImplementation(() => ({
+ status: 'success',
+ importCount: 1,
+ failedImports: [],
+ }));
+ });
+
+ it('should figure out unmatchedReferences', async () => {
+ const component = shallowWithIntl( );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ component.setState({ file: mockFile, isLegacyFile: false });
+ await component.instance().import();
+
+ expect(importFile).toHaveBeenCalledWith(mockFile, true);
+ expect(component.state()).toMatchObject({
+ conflictedIndexPatterns: undefined,
+ conflictedSavedObjectsLinkedToSavedSearches: undefined,
+ conflictedSearchDocs: undefined,
+ importCount: 0,
+ status: 'idle',
+ error: undefined,
+ unmatchedReferences: [
+ {
+ existingIndexPatternId: 'MyIndexPattern*',
+ newIndexPatternId: undefined,
+ list: [
+ {
+ id: '1',
+ type: 'visualization',
+ title: 'My Visualization',
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it('should allow conflict resolution', async () => {
+ const component = shallowWithIntl( );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ component.setState({ file: mockFile, isLegacyFile: false });
+ await component.instance().import();
+
+ // Ensure it looks right
+ component.update();
+ expect(component).toMatchSnapshot();
+
+ // Ensure we can change the resolution
+ component
+ .instance()
+ .onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
+ expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2');
+
+ // Let's resolve now
+ await component
+ .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]')
+ .simulate('click');
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ expect(resolveImportErrors).toMatchSnapshot();
+ });
+
+ it('should handle errors', async () => {
+ const component = shallowWithIntl( );
+
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ resolveImportErrors.mockImplementation(() => ({
+ status: 'success',
+ importCount: 0,
+ failedImports: [
+ {
+ obj: {
+ type: 'visualization',
+ id: '1',
+ },
+ error: {
+ type: 'unknown',
+ },
+ },
+ ],
+ }));
+
+ component.setState({ file: mockFile, isLegacyFile: false });
+
+ // Go through the import flow
+ await component.instance().import();
+ component.update();
+ // Set a resolution
+ component
+ .instance()
+ .onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
+ await component
+ .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]')
+ .simulate('click');
+ // Ensure all promises resolve
+ await new Promise(resolve => process.nextTick(resolve));
+
+ expect(component.state('failedImports')).toEqual([
+ {
+ error: {
+ type: 'unknown',
+ },
+ obj: {
+ id: '1',
+ type: 'visualization',
+ },
+ },
+ ]);
+ expect(component.find('EuiFlyoutBody EuiCallOut')).toMatchSnapshot();
+ });
+ });
+
+ describe('legacy conflicts', () => {
+ const { importLegacyFile } = require('../../../../../lib/import_legacy_file');
const {
resolveSavedObjects,
resolveSavedSearches,
@@ -175,7 +357,7 @@ describe('Flyout', () => {
const mockConflictedSearchDocs = [3];
beforeEach(() => {
- importFile.mockImplementation(() => mockData);
+ importLegacyFile.mockImplementation(() => mockData);
resolveSavedObjects.mockImplementation(() => ({
conflictedIndexPatterns: mockConflictedIndexPatterns,
conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches,
@@ -184,7 +366,7 @@ describe('Flyout', () => {
}));
});
- it('should figure out conflicts', async () => {
+ it('should figure out unmatchedReferences', async () => {
const component = shallowWithIntl( );
// Ensure all promises resolve
@@ -192,10 +374,10 @@ describe('Flyout', () => {
// Ensure the state changes are reflected
component.update();
- component.setState({ file: mockFile });
- await component.instance().import();
+ component.setState({ file: legacyMockFile, isLegacyFile: true });
+ await component.instance().legacyImport();
- expect(importFile).toHaveBeenCalledWith(mockFile);
+ expect(importLegacyFile).toHaveBeenCalledWith(legacyMockFile);
// Remove the last element from data since it should be filtered out
expect(resolveSavedObjects).toHaveBeenCalledWith(
mockData.slice(0, 2).map((doc) => ({ ...doc, _migrationVersion: {} })),
@@ -209,16 +391,16 @@ describe('Flyout', () => {
conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches,
conflictedSearchDocs: mockConflictedSearchDocs,
importCount: 2,
- isLoading: false,
- wasImportSuccessful: false,
- conflicts: [
+ status: 'idle',
+ error: undefined,
+ unmatchedReferences: [
{
existingIndexPatternId: 'MyIndexPattern*',
newIndexPatternId: undefined,
list: [
{
id: 'MyIndexPattern*',
- name: 'MyIndexPattern*',
+ title: 'MyIndexPattern*',
type: 'index-pattern',
},
],
@@ -235,8 +417,8 @@ describe('Flyout', () => {
// Ensure the state changes are reflected
component.update();
- component.setState({ file: mockFile });
- await component.instance().import();
+ component.setState({ file: legacyMockFile, isLegacyFile: true });
+ await component.instance().legacyImport();
// Ensure it looks right
component.update();
@@ -246,7 +428,7 @@ describe('Flyout', () => {
component
.instance()
.onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
- expect(component.state('conflicts')[0].newIndexPatternId).toBe('2');
+ expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2');
// Let's resolve now
await component
@@ -283,10 +465,10 @@ describe('Flyout', () => {
throw new Error('foobar');
});
- component.setState({ file: mockFile });
+ component.setState({ file: legacyMockFile, isLegacyFile: true });
// Go through the import flow
- await component.instance().import();
+ await component.instance().legacyImport();
component.update();
// Set a resolution
component
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js
index 6610b764589c3d..a27875bb425918 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js
@@ -41,8 +41,17 @@ import {
EuiCallOut,
EuiSpacer,
EuiLink,
+ EuiConfirmModal,
+ EuiOverlayMask,
+ EUI_MODAL_CONFIRM_BUTTON,
} from '@elastic/eui';
-import { importFile } from '../../../../lib/import_file';
+import {
+ importFile,
+ importLegacyFile,
+ resolveImportErrors,
+ logLegacyImport,
+ processImportResponse,
+} from '../../../../lib';
import {
resolveSavedObjects,
resolveSavedSearches,
@@ -68,15 +77,16 @@ class FlyoutUI extends Component {
conflictedIndexPatterns: undefined,
conflictedSavedObjectsLinkedToSavedSearches: undefined,
conflictedSearchDocs: undefined,
- conflicts: undefined,
+ unmatchedReferences: undefined,
+ conflictingRecord: undefined,
error: undefined,
file: undefined,
importCount: 0,
indexPatterns: undefined,
isOverwriteAllChecked: true,
- isLoading: false,
loadingMessage: undefined,
- wasImportSuccessful: false,
+ isLegacyFile: false,
+ status: 'idle',
};
}
@@ -99,24 +109,125 @@ class FlyoutUI extends Component {
};
setImportFile = ([file]) => {
- this.setState({ file });
+ if (!file) {
+ this.setState({ file: undefined, isLegacyFile: false });
+ return;
+ }
+ this.setState({
+ file,
+ isLegacyFile: /\.json$/i.test(file.name) || file.type === 'application/json',
+ });
};
+ /**
+ * Import
+ *
+ * Does the initial import of a file, resolveImportErrors then handles errors and retries
+ */
import = async () => {
+ const { intl } = this.props;
+ const { file, isOverwriteAllChecked } = this.state;
+ this.setState({ status: 'loading', error: undefined });
+
+ // Import the file
+ let response;
+ try {
+ response = await importFile(file, isOverwriteAllChecked);
+ } catch (e) {
+ this.setState({
+ status: 'error',
+ error: intl.formatMessage({
+ id: 'kbn.management.objects.objectsTable.flyout.importFileErrorMessage',
+ defaultMessage: 'The file could not be processed.',
+ }),
+ });
+ return;
+ }
+
+ this.setState(processImportResponse(response), () => {
+ // Resolve import errors right away if there's no index patterns to match
+ // This will ask about overwriting each object, etc
+ if (this.state.unmatchedReferences.length === 0) {
+ this.resolveImportErrors();
+ }
+ });
+ }
+
+ /**
+ * Get Conflict Resolutions
+ *
+ * Function iterates through the objects, displays a modal for each asking the user if they wish to overwrite it or not.
+ *
+ * @param {array} objects List of objects to request the user if they wish to overwrite it
+ * @return {Promise} An object with the key being "type:id" and value the resolution chosen by the user
+ */
+ getConflictResolutions = async (objects) => {
+ const resolutions = {};
+ for (const { type, id, title } of objects) {
+ const overwrite = await new Promise((resolve) => {
+ this.setState({
+ conflictingRecord: {
+ id,
+ type,
+ title,
+ done: resolve,
+ },
+ });
+ });
+ resolutions[`${type}:${id}`] = overwrite;
+ this.setState({ conflictingRecord: undefined });
+ }
+ return resolutions;
+ }
+
+ /**
+ * Resolve Import Errors
+ *
+ * Function goes through the failedImports and tries to resolve the issues.
+ */
+ resolveImportErrors = async () => {
+ const { intl } = this.props;
+
+ this.setState({
+ error: undefined,
+ status: 'loading',
+ loadingMessage: undefined,
+ });
+
+ try {
+ const updatedState = await resolveImportErrors({
+ state: this.state,
+ getConflictResolutions: this.getConflictResolutions,
+ });
+ this.setState(updatedState);
+ } catch (e) {
+ this.setState({
+ status: 'error',
+ error: intl.formatMessage({
+ id: 'kbn.management.objects.objectsTable.flyout.resolveImportErrorsFileErrorMessage',
+ defaultMessage: 'The file could not be processed.',
+ }),
+ });
+ }
+ }
+
+ legacyImport = async () => {
const { services, indexPatterns, intl } = this.props;
const { file, isOverwriteAllChecked } = this.state;
- this.setState({ isLoading: true, error: undefined });
+ this.setState({ status: 'loading', error: undefined });
- let contents;
+ // Log warning on server, don't wait for response
+ logLegacyImport();
+ let contents;
try {
- contents = await importFile(file);
+ contents = await importLegacyFile(file);
} catch (e) {
this.setState({
- isLoading: false,
+ status: 'error',
error: intl.formatMessage({
- id: 'kbn.management.objects.objectsTable.flyout.importFileErrorMessage',
+ id: 'kbn.management.objects.objectsTable.flyout.importLegacyFileErrorMessage',
defaultMessage: 'The file could not be processed.',
}),
});
@@ -125,7 +236,7 @@ class FlyoutUI extends Component {
if (!Array.isArray(contents)) {
this.setState({
- isLoading: false,
+ status: 'error',
error: intl.formatMessage({
id: 'kbn.management.objects.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage',
defaultMessage: 'Saved objects file format is invalid and cannot be imported.',
@@ -162,7 +273,7 @@ class FlyoutUI extends Component {
const byId = groupBy(conflictedIndexPatterns, ({ obj }) =>
obj.searchSource.getOwnField('index')
);
- const conflicts = Object.entries(byId).reduce(
+ const unmatchedReferences = Object.entries(byId).reduce(
(accum, [existingIndexPatternId, list]) => {
accum.push({
existingIndexPatternId,
@@ -170,7 +281,7 @@ class FlyoutUI extends Component {
list: list.map(({ doc }) => ({
id: existingIndexPatternId,
type: doc._type,
- name: doc._source.title,
+ title: doc._source.title,
})),
});
return accum;
@@ -183,19 +294,18 @@ class FlyoutUI extends Component {
conflictedSavedObjectsLinkedToSavedSearches,
conflictedSearchDocs,
failedImports,
- conflicts,
+ unmatchedReferences,
importCount: importedObjectCount,
- isLoading: false,
- wasImportSuccessful: conflicts.length === 0,
+ status: unmatchedReferences.length === 0 ? 'success' : 'idle',
});
};
- get hasConflicts() {
- return this.state.conflicts && this.state.conflicts.length > 0;
+ get hasUnmatchedReferences() {
+ return this.state.unmatchedReferences && this.state.unmatchedReferences.length > 0;
}
get resolutions() {
- return this.state.conflicts.reduce(
+ return this.state.unmatchedReferences.reduce(
(accum, { existingIndexPatternId, newIndexPatternId }) => {
if (newIndexPatternId) {
accum.push({
@@ -209,7 +319,7 @@ class FlyoutUI extends Component {
);
}
- confirmImport = async () => {
+ confirmLegacyImport = async () => {
const {
conflictedIndexPatterns,
isOverwriteAllChecked,
@@ -222,20 +332,20 @@ class FlyoutUI extends Component {
this.setState({
error: undefined,
- isLoading: true,
+ status: 'loading',
loadingMessage: undefined,
});
let importCount = this.state.importCount;
- if (this.hasConflicts) {
+ if (this.hasUnmatchedReferences) {
try {
const resolutions = this.resolutions;
// Do not Promise.all these calls as the order matters
this.setState({
loadingMessage: intl.formatMessage({
- id: 'kbn.management.objects.objectsTable.flyout.confirmImport.resolvingConflictsLoadingMessage',
+ id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage',
defaultMessage: 'Resolving conflicts…',
}),
});
@@ -248,7 +358,7 @@ class FlyoutUI extends Component {
}
this.setState({
loadingMessage: intl.formatMessage({
- id: 'kbn.management.objects.objectsTable.flyout.confirmImport.savingConflictsLoadingMessage',
+ id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage',
defaultMessage: 'Saving conflicts…',
}),
});
@@ -258,7 +368,7 @@ class FlyoutUI extends Component {
);
this.setState({
loadingMessage: intl.formatMessage({
- id: 'kbn.management.objects.objectsTable.flyout.confirmImport.savedSearchAreLinkedProperlyLoadingMessage',
+ id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage',
defaultMessage: 'Ensure saved searches are linked properly…',
}),
});
@@ -270,7 +380,7 @@ class FlyoutUI extends Component {
);
this.setState({
loadingMessage: intl.formatMessage({
- id: 'kbn.management.objects.objectsTable.flyout.confirmImport.retryingFailedObjectsLoadingMessage',
+ id: 'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage',
defaultMessage: 'Retrying failed objects…',
}),
});
@@ -281,20 +391,20 @@ class FlyoutUI extends Component {
} catch (e) {
this.setState({
error: e.message,
- isLoading: false,
+ status: 'error',
loadingMessage: undefined,
});
return;
}
}
- this.setState({ isLoading: false, wasImportSuccessful: true, importCount });
+ this.setState({ status: 'success', importCount });
};
onIndexChanged = (id, e) => {
const value = e.target.value;
this.setState(state => {
- const conflictIndex = state.conflicts.findIndex(
+ const conflictIndex = state.unmatchedReferences.findIndex(
conflict => conflict.existingIndexPatternId === id
);
if (conflictIndex === -1) {
@@ -302,23 +412,23 @@ class FlyoutUI extends Component {
}
return {
- conflicts: [
- ...state.conflicts.slice(0, conflictIndex),
+ unmatchedReferences: [
+ ...state.unmatchedReferences.slice(0, conflictIndex),
{
- ...state.conflicts[conflictIndex],
+ ...state.unmatchedReferences[conflictIndex],
newIndexPatternId: value,
},
- ...state.conflicts.slice(conflictIndex + 1),
+ ...state.unmatchedReferences.slice(conflictIndex + 1),
],
};
});
};
- renderConflicts() {
- const { conflicts } = this.state;
+ renderUnmatchedReferences() {
+ const { unmatchedReferences } = this.state;
const { intl } = this.props;
- if (!conflicts) {
+ if (!unmatchedReferences) {
return null;
}
@@ -362,7 +472,7 @@ class FlyoutUI extends Component {
render: list => {
return (
- {take(list, 3).map((obj, key) => {obj.name} )}
+ {take(list, 3).map((obj, key) => {obj.title} )}
);
},
@@ -402,7 +512,7 @@ class FlyoutUI extends Component {
return (
@@ -410,9 +520,9 @@ class FlyoutUI extends Component {
}
renderError() {
- const { error } = this.state;
+ const { error, status } = this.state;
- if (!error) {
+ if (status !== 'error') {
return null;
}
@@ -433,16 +543,17 @@ class FlyoutUI extends Component {
}
renderBody() {
+ const { intl } = this.props;
const {
- isLoading,
+ status,
loadingMessage,
isOverwriteAllChecked,
- wasImportSuccessful,
importCount,
failedImports = [],
+ isLegacyFile,
} = this.state;
- if (isLoading) {
+ if (status === 'loading') {
return (
@@ -456,7 +567,8 @@ class FlyoutUI extends Component {
);
}
- if (failedImports.length && !this.hasConflicts) {
+ // Kept backwards compatible logic
+ if (failedImports.length && (!this.hasUnmatchedReferences || (isLegacyFile === false && status === 'success'))) {
return (
- {failedImports.map(({ error }) => getField(error, 'body.message', error.message || '')).join(' ')}
+ {failedImports.map(({ error, obj }) => {
+ if (error.type === 'missing_references') {
+ return error.references.map((reference) => {
+ return intl.formatMessage(
+ {
+ id: 'kbn.management.objects.objectsTable.flyout.importFailedMissingReference',
+ defaultMessage: '{type} [id={id}] could not locate {refType} [id={refId}]',
+ },
+ {
+ id: obj.id,
+ type: obj.type,
+ refId: reference.id,
+ refType: reference.type,
+ }
+ );
+ });
+ }
+ return getField(error, 'body.message', error.message || '');
+ }).join(' ')}
);
}
- if (wasImportSuccessful) {
+ if (status === 'success') {
if (importCount === 0) {
return (
)}
>
@@ -561,12 +691,12 @@ class FlyoutUI extends Component {
}
renderFooter() {
- const { isLoading, wasImportSuccessful } = this.state;
+ const { status } = this.state;
const { done, close } = this.props;
let confirmButton;
- if (wasImportSuccessful) {
+ if (status === 'success') {
confirmButton = (
);
- } else if (this.hasConflicts) {
+ } else if (this.hasUnmatchedReferences) {
confirmButton = (
-
+ let legacyFileWarning;
+ if (this.state.isLegacyFile) {
+ legacyFileWarning = (
+
+ )}
+ color="warning"
+ iconType="help"
+ >
+
+
+
+
+ );
+ }
+
+ let indexPatternConflictsWarning;
+ if (this.hasUnmatchedReferences) {
+ indexPatternConflictsWarning = (
-
+ );
+ }
+
+ if (!legacyFileWarning && !indexPatternConflictsWarning) {
+ return null;
+ }
+
+ return (
+
+ {legacyFileWarning &&
+
+
+ {legacyFileWarning}
+
+ }
+ {indexPatternConflictsWarning &&
+
+
+ {indexPatternConflictsWarning}
+
+ }
);
}
+ overwriteConfirmed() {
+ this.state.conflictingRecord.done(true);
+ }
+
+ overwriteSkipped() {
+ this.state.conflictingRecord.done(false);
+ }
+
render() {
- const { close } = this.props;
+ const { close, intl } = this.props;
+
+ let confirmOverwriteModal;
+ if (this.state.conflictingRecord) {
+ confirmOverwriteModal = (
+
+
+
+
+
+
+ );
+ }
return (
@@ -695,6 +914,7 @@ class FlyoutUI extends Component {
{this.renderFooter()}
+ {confirmOverwriteModal}
);
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js
index fabbfce0395e3b..90e9a694689e03 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js
@@ -20,6 +20,8 @@
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
+jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
+
jest.mock('ui/errors', () => ({
SavedObjectNotFound: class SavedObjectNotFound extends Error {
constructor(options) {
@@ -37,6 +39,14 @@ jest.mock('ui/chrome', () => ({
addBasePath: () => ''
}));
+jest.mock('../../../../../lib/fetch_export_by_type', () => ({
+ fetchExportByType: jest.fn(),
+}));
+
+jest.mock('../../../../../lib/fetch_export_objects', () => ({
+ fetchExportObjects: jest.fn(),
+}));
+
import { Relationships } from '../relationships';
describe('Relationships', () => {
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap
index 543599fd02a54d..d749291b9fce32 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/__snapshots__/table.test.js.snap
@@ -42,22 +42,81 @@ exports[`Table should render normally 1`] = `
values={Object {}}
/>
,
-
+
+
+ }
+ closePopover={[Function]}
+ hasArrow={true}
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="m"
>
-
- ,
+
+ }
+ labelType="label"
+ >
+
+ }
+ name="includeReferencesDeep"
+ onChange={[Function]}
+ />
+
+
+
+
+
+
+ ,
]
}
/>
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js
index 1228667a647f51..8154a7cb480c8e 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/__jest__/table.test.js
@@ -22,6 +22,8 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { keyCodes } from '@elastic/eui/lib/services';
+jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
+
jest.mock('ui/errors', () => ({
SavedObjectNotFound: class SavedObjectNotFound extends Error {
constructor(options) {
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js
index 09ba15cc16b4b6..c88b82633eb0a6 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/table/table.js
@@ -28,7 +28,10 @@ import {
EuiLink,
EuiSpacer,
EuiToolTip,
- EuiFormErrorText
+ EuiFormErrorText,
+ EuiPopover,
+ EuiSwitch,
+ EuiFormRow
} from '@elastic/eui';
import { getSavedObjectLabel, getSavedObjectIcon } from '../../../../lib';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
@@ -65,6 +68,8 @@ class TableUI extends PureComponent {
state = {
isSearchTextValid: true,
parseErrorMessage: null,
+ isExportPopoverOpen: false,
+ isIncludeReferencesDeepChecked: true,
}
onChange = ({ query, error }) => {
@@ -83,6 +88,29 @@ class TableUI extends PureComponent {
this.props.onQueryChange({ query });
}
+ closeExportPopover = () => {
+ this.setState({ isExportPopoverOpen: false });
+ }
+
+ toggleExportPopoverVisibility = () => {
+ this.setState(state => ({
+ isExportPopoverOpen: !state.isExportPopoverOpen
+ }));
+ }
+
+ toggleIsIncludeReferencesDeepChecked = () => {
+ this.setState(state => ({
+ isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked,
+ }));
+ }
+
+ onExportClick = () => {
+ const { onExport } = this.props;
+ const { isIncludeReferencesDeepChecked } = this.state;
+ onExport(isIncludeReferencesDeepChecked);
+ this.setState({ isExportPopoverOpen: false });
+ }
+
render() {
const {
pageIndex,
@@ -94,7 +122,6 @@ class TableUI extends PureComponent {
filterOptions,
selectionConfig: selection,
onDelete,
- onExport,
selectedSavedObjects,
onTableChange,
goInApp,
@@ -216,6 +243,20 @@ class TableUI extends PureComponent {
);
}
+ const button = (
+
+
+
+ );
+
return (
,
-
-
- ,
+
+ )}
+ >
+
+ )}
+ checked={this.state.isIncludeReferencesDeepChecked}
+ onChange={this.toggleIsIncludeReferencesDeepChecked}
+ />
+
+
+
+
+
+
+ ,
]}
/>
{queryParseError}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js
index 9a1a1278415ee8..cd8996e2e28cb7 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js
@@ -17,9 +17,10 @@
* under the License.
*/
+import { saveAs } from '@elastic/filesaver';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { debounce, flattenDeep } from 'lodash';
+import { debounce } from 'lodash';
import { Header } from './components/header';
import { Flyout } from './components/flyout';
import { Relationships } from './components/relationships';
@@ -38,16 +39,26 @@ import {
EuiCheckboxGroup,
EuiToolTip,
EuiPageContent,
+ EuiSwitch,
+ EuiModal,
+ EuiModalHeader,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiButtonEmpty,
+ EuiButton,
+ EuiModalHeaderTitle,
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem
} from '@elastic/eui';
import {
- retrieveAndExportDocs,
- scanAllTypes,
- saveToFile,
parseQuery,
getSavedObjectIcon,
getSavedObjectCounts,
getRelationships,
getSavedObjectLabel,
+ fetchExportObjects,
+ fetchExportByType,
} from '../../lib';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
@@ -97,6 +108,7 @@ class ObjectsTableUI extends Component {
isDeleting: false,
exportAllOptions: [],
exportAllSelectedOptions: {},
+ isIncludeReferencesDeepChecked: true,
};
}
@@ -278,17 +290,23 @@ class ObjectsTableUI extends Component {
});
};
- onExport = async () => {
- const { savedObjectsClient } = this.props;
+ onExport = async (includeReferencesDeep) => {
+ const { intl } = this.props;
const { selectedSavedObjects } = this.state;
- const objects = await savedObjectsClient.bulkGet(selectedSavedObjects);
- await retrieveAndExportDocs(objects.savedObjects, savedObjectsClient);
+ const objectsToExport = selectedSavedObjects.map(obj => ({ id: obj.id, type: obj.type }));
+ const blob = await fetchExportObjects(objectsToExport, includeReferencesDeep);
+ saveAs(blob, 'export.ndjson');
+ toastNotifications.addSuccess({
+ title: intl.formatMessage({
+ id: 'kbn.management.objects.objectsTable.export.successNotification',
+ defaultMessage: 'Your file is downloading in the background',
+ }),
+ });
};
onExportAll = async () => {
- const { $http } = this.props;
- const { exportAllSelectedOptions } = this.state;
-
+ const { intl } = this.props;
+ const { exportAllSelectedOptions, isIncludeReferencesDeepChecked } = this.state;
const exportTypes = Object.entries(exportAllSelectedOptions).reduce(
(accum, [id, selected]) => {
if (selected) {
@@ -298,8 +316,15 @@ class ObjectsTableUI extends Component {
},
[]
);
- const results = await scanAllTypes($http, exportTypes);
- saveToFile(JSON.stringify(flattenDeep(results), null, 2));
+ const blob = await fetchExportByType(exportTypes, isIncludeReferencesDeepChecked);
+ saveAs(blob, 'export.ndjson');
+ toastNotifications.addSuccess({
+ title: intl.formatMessage({
+ id: 'kbn.management.objects.objectsTable.exportAll.successNotification',
+ defaultMessage: 'Your file is downloading in the background',
+ }),
+ });
+ this.setState({ isShowingExportAllOptionsModal: false });
};
finishImport = () => {
@@ -512,12 +537,23 @@ class ObjectsTableUI extends Component {
);
}
+ changeIncludeReferencesDeep = () => {
+ this.setState(state => ({
+ isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked,
+ }));
+ }
+
+ closeExportAllModal = () => {
+ this.setState({ isShowingExportAllOptionsModal: false });
+ }
+
renderExportAllOptionsModal() {
const {
isShowingExportAllOptionsModal,
filteredItemCount,
exportAllOptions,
exportAllSelectedOptions,
+ isIncludeReferencesDeepChecked,
} = this.state;
if (!isShowingExportAllOptionsModal) {
@@ -526,53 +562,84 @@ class ObjectsTableUI extends Component {
return (
- )}
- onCancel={() =>
- this.setState({ isShowingExportAllOptionsModal: false })
- }
- onConfirm={this.onExportAll}
- cancelButtonText={(
-
- )}
- confirmButtonText={(
-
- )}
- defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
+
-
-
-
- {
- const newExportAllSelectedOptions = {
- ...exportAllSelectedOptions,
- ...{
- [optionId]: !exportAllSelectedOptions[optionId],
- },
- };
-
- this.setState({
- exportAllSelectedOptions: newExportAllSelectedOptions,
- });
- }}
- />
-
+
+
+
+
+
+
+
+
+
+
+ {
+ const newExportAllSelectedOptions = {
+ ...exportAllSelectedOptions,
+ ...{
+ [optionId]: !exportAllSelectedOptions[optionId],
+ },
+ };
+
+ this.setState({
+ exportAllSelectedOptions: newExportAllSelectedOptions,
+ });
+ }}
+ />
+
+
+
+
+
+
+ )}
+ checked={isIncludeReferencesDeepChecked}
+ onChange={this.changeIncludeReferencesDeep}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_file.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_legacy_file.test.js
similarity index 90%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_file.test.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_legacy_file.test.js
index a7c78ae9a9122c..57b8557fb9afe5 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_file.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_legacy_file.test.js
@@ -17,7 +17,7 @@
* under the License.
*/
-import { importFile } from '../import_file';
+import { importLegacyFile } from '../import_legacy_file';
describe('importFile', () => {
it('should import a file', async () => {
@@ -33,7 +33,7 @@ describe('importFile', () => {
const file = 'foo';
- const imported = await importFile(file, FileReader);
+ const imported = await importLegacyFile(file, FileReader);
expect(imported).toEqual({ text: file });
});
@@ -51,7 +51,7 @@ describe('importFile', () => {
const file = 'foo';
try {
- await importFile(file, FileReader);
+ await importLegacyFile(file, FileReader);
} catch (e) {
// There isn't a great way to handle throwing exceptions
// with async/await but this seems to work :shrug:
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/process_import_response.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/process_import_response.test.js
new file mode 100644
index 00000000000000..dc4d81dae8081f
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/process_import_response.test.js
@@ -0,0 +1,146 @@
+/*
+ * 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 { processImportResponse } from '../process_import_response';
+
+describe('processImportResponse()', () => {
+ test('works when no errors exist in the response', () => {
+ const response = {
+ success: true,
+ successCount: 0,
+ };
+ const result = processImportResponse(response);
+ expect(result.status).toBe('success');
+ expect(result.importCount).toBe(0);
+ });
+
+ test('conflict errors get added to failedImports', () => {
+ const response = {
+ success: false,
+ successCount: 0,
+ errors: [
+ {
+ obj: {
+ type: 'a',
+ id: '1',
+ },
+ error: {
+ type: 'conflict',
+ },
+ },
+ ],
+ };
+ const result = processImportResponse(response);
+ expect(result.failedImports).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "error": Object {
+ "type": "conflict",
+ },
+ "obj": Object {
+ "obj": Object {
+ "id": "1",
+ "type": "a",
+ },
+ },
+ },
+]
+`);
+ });
+
+ test('unknown errors get added to failedImports', () => {
+ const response = {
+ success: false,
+ successCount: 0,
+ errors: [
+ {
+ obj: {
+ type: 'a',
+ id: '1',
+ },
+ error: {
+ type: 'unknown',
+ },
+ },
+ ],
+ };
+ const result = processImportResponse(response);
+ expect(result.failedImports).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "error": Object {
+ "type": "unknown",
+ },
+ "obj": Object {
+ "obj": Object {
+ "id": "1",
+ "type": "a",
+ },
+ },
+ },
+]
+`);
+ });
+
+ test('missing references get added to failedImports', () => {
+ const response = {
+ success: false,
+ successCount: 0,
+ errors: [
+ {
+ obj: {
+ type: 'a',
+ id: '1',
+ },
+ error: {
+ type: 'missing_references',
+ references: [
+ {
+ type: 'index-pattern',
+ id: '2',
+ },
+ ],
+ },
+ },
+ ],
+ };
+ const result = processImportResponse(response);
+ expect(result.failedImports).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "error": Object {
+ "references": Array [
+ Object {
+ "id": "2",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "missing_references",
+ },
+ "obj": Object {
+ "obj": Object {
+ "id": "1",
+ "type": "a",
+ },
+ },
+ },
+]
+`);
+ });
+});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/resolve_import_errors.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/resolve_import_errors.test.js
new file mode 100644
index 00000000000000..ecdf3d5abced66
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/resolve_import_errors.test.js
@@ -0,0 +1,377 @@
+/*
+ * 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 { resolveImportErrors } from '../resolve_import_errors';
+
+jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
+
+function getFormData(form) {
+ const formData = {};
+ for (const [key, val] of form.entries()) {
+ if (key === 'retries') {
+ formData[key] = JSON.parse(val);
+ continue;
+ }
+ formData[key] = val;
+ }
+ return formData;
+}
+
+describe('resolveImportErrors', () => {
+ const getConflictResolutions = jest.fn();
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ test('works with empty import failures', async () => {
+ const result = await resolveImportErrors({
+ getConflictResolutions,
+ state: {
+ importCount: 0,
+ },
+ });
+ expect(result).toMatchInlineSnapshot(`
+Object {
+ "failedImports": Array [],
+ "importCount": 0,
+ "status": "success",
+}
+`);
+ });
+
+ test(`doesn't retry if only unknown failures are passed in`, async () => {
+ const result = await resolveImportErrors({
+ getConflictResolutions,
+ state: {
+ importCount: 0,
+ failedImports: [
+ {
+ obj: {
+ type: 'a',
+ id: '1',
+ },
+ error: {
+ type: 'unknown',
+ },
+ },
+ ],
+ },
+ });
+ expect(result).toMatchInlineSnapshot(`
+Object {
+ "failedImports": Array [
+ Object {
+ "error": Object {
+ "type": "unknown",
+ },
+ "obj": Object {
+ "id": "1",
+ "type": "a",
+ },
+ },
+ ],
+ "importCount": 0,
+ "status": "success",
+}
+`);
+ });
+
+ test('resolves conflicts', async () => {
+ const { kfetch } = require('ui/kfetch');
+ kfetch.mockResolvedValueOnce({
+ success: true,
+ successCount: 1,
+ });
+ getConflictResolutions.mockReturnValueOnce({
+ 'a:1': true,
+ 'a:2': false,
+ });
+ const result = await resolveImportErrors({
+ getConflictResolutions,
+ state: {
+ importCount: 0,
+ failedImports: [
+ {
+ obj: {
+ type: 'a',
+ id: '1',
+ },
+ error: {
+ type: 'conflict',
+ },
+ },
+ {
+ obj: {
+ type: 'a',
+ id: '2',
+ },
+ error: {
+ type: 'conflict',
+ },
+ },
+ ],
+ },
+ });
+ expect(result).toMatchInlineSnapshot(`
+Object {
+ "failedImports": Array [],
+ "importCount": 1,
+ "status": "success",
+}
+`);
+ const formData = getFormData(kfetch.mock.calls[0][0].body);
+ expect(formData).toMatchInlineSnapshot(`
+Object {
+ "file": "undefined",
+ "retries": Array [
+ Object {
+ "id": "1",
+ "overwrite": true,
+ "replaceReferences": Array [],
+ "type": "a",
+ },
+ ],
+}
+`);
+ });
+
+ test('resolves missing references', async () => {
+ const { kfetch } = require('ui/kfetch');
+ kfetch.mockResolvedValueOnce({
+ success: true,
+ successCount: 2,
+ });
+ getConflictResolutions.mockResolvedValueOnce({});
+ const result = await resolveImportErrors({
+ getConflictResolutions,
+ state: {
+ importCount: 0,
+ unmatchedReferences: [
+ {
+ existingIndexPatternId: '2',
+ newIndexPatternId: '3',
+ },
+ ],
+ failedImports: [
+ {
+ obj: {
+ type: 'a',
+ id: '1',
+ },
+ error: {
+ type: 'missing_references',
+ references: [
+ {
+ type: 'index-pattern',
+ id: '2',
+ },
+ ],
+ blocking: [
+ {
+ type: 'a',
+ id: '2',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ });
+ expect(result).toMatchInlineSnapshot(`
+Object {
+ "failedImports": Array [],
+ "importCount": 2,
+ "status": "success",
+}
+`);
+ const formData = getFormData(kfetch.mock.calls[0][0].body);
+ expect(formData).toMatchInlineSnapshot(`
+Object {
+ "file": "undefined",
+ "retries": Array [
+ Object {
+ "id": "1",
+ "overwrite": false,
+ "replaceReferences": Array [
+ Object {
+ "from": "2",
+ "to": "3",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "a",
+ },
+ Object {
+ "id": "2",
+ "type": "a",
+ },
+ ],
+}
+`);
+ });
+
+ test(`doesn't resolve missing references if newIndexPatternId isn't defined`, async () => {
+ getConflictResolutions.mockResolvedValueOnce({});
+ const result = await resolveImportErrors({
+ getConflictResolutions,
+ state: {
+ importCount: 0,
+ unmatchedReferences: [
+ {
+ existingIndexPatternId: '2',
+ newIndexPatternId: undefined,
+ },
+ ],
+ failedImports: [
+ {
+ obj: {
+ type: 'a',
+ id: '1',
+ },
+ error: {
+ type: 'missing_references',
+ references: [
+ {
+ type: 'index-pattern',
+ id: '2',
+ },
+ ],
+ blocking: [
+ {
+ type: 'a',
+ id: '2',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ });
+ expect(result).toMatchInlineSnapshot(`
+Object {
+ "failedImports": Array [],
+ "importCount": 0,
+ "status": "success",
+}
+`);
+ });
+
+ test('handles missing references then conflicts on the same errored objects', async () => {
+ const { kfetch } = require('ui/kfetch');
+ kfetch.mockResolvedValueOnce({
+ success: false,
+ successCount: 0,
+ errors: [
+ {
+ type: 'a',
+ id: '1',
+ error: {
+ type: 'conflict',
+ },
+ },
+ ],
+ });
+ kfetch.mockResolvedValueOnce({
+ success: true,
+ successCount: 1,
+ });
+ getConflictResolutions.mockResolvedValueOnce({});
+ getConflictResolutions.mockResolvedValueOnce({
+ 'a:1': true,
+ });
+ const result = await resolveImportErrors({
+ getConflictResolutions,
+ state: {
+ importCount: 0,
+ unmatchedReferences: [
+ {
+ existingIndexPatternId: '2',
+ newIndexPatternId: '3',
+ },
+ ],
+ failedImports: [
+ {
+ obj: {
+ type: 'a',
+ id: '1',
+ },
+ error: {
+ type: 'missing_references',
+ references: [
+ {
+ type: 'index-pattern',
+ id: '2',
+ },
+ ],
+ blocking: [],
+ },
+ },
+ ],
+ },
+ });
+ expect(result).toMatchInlineSnapshot(`
+Object {
+ "failedImports": Array [],
+ "importCount": 1,
+ "status": "success",
+}
+`);
+ const formData1 = getFormData(kfetch.mock.calls[0][0].body);
+ expect(formData1).toMatchInlineSnapshot(`
+Object {
+ "file": "undefined",
+ "retries": Array [
+ Object {
+ "id": "1",
+ "overwrite": false,
+ "replaceReferences": Array [
+ Object {
+ "from": "2",
+ "to": "3",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "a",
+ },
+ ],
+}
+`);
+ const formData2 = getFormData(kfetch.mock.calls[1][0].body);
+ expect(formData2).toMatchInlineSnapshot(`
+Object {
+ "file": "undefined",
+ "retries": Array [
+ Object {
+ "id": "1",
+ "overwrite": true,
+ "replaceReferences": Array [
+ Object {
+ "from": "2",
+ "to": "3",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "a",
+ },
+ ],
+}
+`);
+ });
+});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/retrieve_and_export_docs.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/retrieve_and_export_docs.test.js
deleted file mode 100644
index 810b660561b5c2..00000000000000
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/retrieve_and_export_docs.test.js
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { retrieveAndExportDocs } from '../retrieve_and_export_docs';
-
-jest.mock('../save_to_file', () => ({
- saveToFile: jest.fn(),
-}));
-
-jest.mock('ui/errors', () => ({
- SavedObjectNotFound: class SavedObjectNotFound extends Error {
- constructor(options) {
- super();
- for (const option in options) {
- if (options.hasOwnProperty(option)) {
- this[option] = options[option];
- }
- }
- }
- },
-}));
-
-jest.mock('ui/chrome', () => ({
- addBasePath: () => {},
-}));
-
-describe('retrieveAndExportDocs', () => {
- let saveToFile;
-
- beforeEach(() => {
- saveToFile = require('../save_to_file').saveToFile;
- saveToFile.mockClear();
- });
-
- it('should fetch all', async () => {
- const savedObjectsClient = {
- bulkGet: jest.fn().mockImplementation(() => ({
- savedObjects: [],
- })),
- };
-
- const objs = [1, 2, 3];
- await retrieveAndExportDocs(objs, savedObjectsClient);
- expect(savedObjectsClient.bulkGet.mock.calls.length).toBe(1);
- expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith(objs);
- });
-
- it('should use the saveToFile utility', async () => {
- const savedObjectsClient = {
- bulkGet: jest.fn().mockImplementation(() => ({
- savedObjects: [
- {
- id: 1,
- type: 'index-pattern',
- attributes: {
- title: 'foobar',
- },
- },
- {
- id: 2,
- type: 'search',
- attributes: {
- title: 'just the foo',
- },
- },
- ],
- })),
- };
-
- const objs = [1, 2, 3];
- await retrieveAndExportDocs(objs, savedObjectsClient);
- expect(saveToFile.mock.calls.length).toBe(1);
- expect(saveToFile).toHaveBeenCalledWith(
- JSON.stringify(
- [
- {
- _id: 1,
- _type: 'index-pattern',
- _source: { title: 'foobar' },
- },
- {
- _id: 2,
- _type: 'search',
- _source: { title: 'just the foo' },
- },
- ],
- null,
- 2
- )
- );
- });
-});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/save_to_file.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type.js
similarity index 66%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/save_to_file.test.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type.js
index 2717a0df638bb1..71c022b9d3998f 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/save_to_file.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type.js
@@ -17,22 +17,15 @@
* under the License.
*/
-import { saveToFile } from '../save_to_file';
+import { kfetch } from 'ui/kfetch';
-jest.mock('@elastic/filesaver', () => ({
- saveAs: jest.fn(),
-}));
-
-describe('saveToFile', () => {
- let saveAs;
-
- beforeEach(() => {
- saveAs = require('@elastic/filesaver').saveAs;
- saveAs.mockClear();
- });
-
- it('should use the file saver utility', async () => {
- saveToFile(JSON.stringify({ foo: 1 }));
- expect(saveAs.mock.calls.length).toBe(1);
+export async function fetchExportByType(types, includeReferencesDeep = false) {
+ return await kfetch({
+ method: 'POST',
+ pathname: '/api/saved_objects/_export',
+ body: JSON.stringify({
+ type: types,
+ includeReferencesDeep,
+ }),
});
-});
+}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.js
new file mode 100644
index 00000000000000..2521113f53752d
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { kfetch } from 'ui/kfetch';
+
+export async function fetchExportObjects(objects, includeReferencesDeep = false) {
+ return await kfetch({
+ method: 'POST',
+ pathname: '/api/saved_objects/_export',
+ body: JSON.stringify({
+ objects,
+ includeReferencesDeep,
+ }),
+ });
+}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js
index 8686de29be4f8f..12630501663162 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js
@@ -17,16 +17,21 @@
* under the License.
*/
-export async function importFile(file, FileReader = window.FileReader) {
- return new Promise((resolve, reject) => {
- const fr = new FileReader();
- fr.onload = ({ target: { result } }) => {
- try {
- resolve(JSON.parse(result));
- } catch (e) {
- reject(e);
- }
- };
- fr.readAsText(file);
+import { kfetch } from 'ui/kfetch';
+
+export async function importFile(file, overwriteAll = false) {
+ const formData = new FormData();
+ formData.append('file', file);
+ return await kfetch({
+ method: 'POST',
+ pathname: '/api/saved_objects/_import',
+ body: formData,
+ headers: {
+ // Important to be undefined, it forces proper headers to be set for FormData
+ 'Content-Type': undefined,
+ },
+ query: {
+ overwrite: overwriteAll
+ },
});
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.js
new file mode 100644
index 00000000000000..12aaf46b294457
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export async function importLegacyFile(file, FileReader = window.FileReader) {
+ return new Promise((resolve, reject) => {
+ const fr = new FileReader();
+ fr.onload = ({ target: { result } }) => {
+ try {
+ resolve(JSON.parse(result));
+ } catch (e) {
+ reject(e);
+ }
+ };
+ fr.readAsText(file);
+ });
+}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js
index e711064a097cc9..c4e0602710b59e 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js
@@ -17,14 +17,17 @@
* under the License.
*/
+export * from './fetch_export_by_type';
+export * from './fetch_export_objects';
export * from './get_in_app_url';
export * from './get_relationships';
export * from './get_saved_object_counts';
export * from './get_saved_object_icon';
export * from './get_saved_object_label';
export * from './import_file';
+export * from './import_legacy_file';
export * from './parse_query';
+export * from './resolve_import_errors';
export * from './resolve_saved_objects';
-export * from './retrieve_and_export_docs';
-export * from './save_to_file';
-export * from './scan_all_types';
+export * from './log_legacy_import';
+export * from './process_import_response';
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/save_to_file.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.js
similarity index 81%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/save_to_file.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.js
index 1fa33161a0141d..9bbafe3e69c988 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/save_to_file.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.js
@@ -17,9 +17,11 @@
* under the License.
*/
-import { saveAs } from '@elastic/filesaver';
+import { kfetch } from 'ui/kfetch';
-export function saveToFile(resultsJson) {
- const blob = new Blob([resultsJson], { type: 'application/json' });
- saveAs(blob, 'export.json');
+export async function logLegacyImport() {
+ return await kfetch({
+ method: 'POST',
+ pathname: '/api/saved_objects/_log_legacy_import',
+ });
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.js
new file mode 100644
index 00000000000000..51ed2062818cca
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.js
@@ -0,0 +1,54 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export function processImportResponse(response) {
+ // Go through the failures and split between unmatchedReferences and failedImports
+ const failedImports = [];
+ const unmatchedReferences = new Map();
+ for (const { error, ...obj } of response.errors || []) {
+ failedImports.push({ obj, error });
+ if (error.type !== 'missing_references') {
+ continue;
+ }
+ // Currently only supports resolving references on index patterns
+ const indexPatternRefs = error.references.filter(ref => ref.type === 'index-pattern');
+ for (const missingReference of indexPatternRefs) {
+ const conflict = unmatchedReferences.get(`${missingReference.type}:${missingReference.id}`) || {
+ existingIndexPatternId: missingReference.id,
+ list: [],
+ newIndexPatternId: undefined,
+ };
+ conflict.list.push(obj);
+ unmatchedReferences.set(`${missingReference.type}:${missingReference.id}`, conflict);
+ }
+ }
+
+ return {
+ failedImports,
+ unmatchedReferences: Array.from(unmatchedReferences.values()),
+ // Import won't be successful in the scenario unmatched references exist, import API returned errors of type unknown or import API
+ // returned errors of type missing_references.
+ status: unmatchedReferences.size === 0 && !failedImports.some(issue => issue.error.type === 'conflict')
+ ? 'success'
+ : 'idle',
+ importCount: response.successCount,
+ conflictedSavedObjectsLinkedToSavedSearches: undefined,
+ conflictedSearchDocs: undefined,
+ };
+}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.js
new file mode 100644
index 00000000000000..15519c515733e8
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.js
@@ -0,0 +1,151 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { kfetch } from 'ui/kfetch';
+
+async function callResolveImportErrorsApi(file, retries) {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('retries', JSON.stringify(retries));
+ return await kfetch({
+ method: 'POST',
+ pathname: '/api/saved_objects/_resolve_import_errors',
+ headers: {
+ // Important to be undefined, it forces proper headers to be set for FormData
+ 'Content-Type': undefined,
+ },
+ body: formData,
+ });
+}
+
+function mapImportFailureToRetryObject({ failure, overwriteDecisionCache, replaceReferencesCache, state }) {
+ const { isOverwriteAllChecked, unmatchedReferences } = state;
+ const isOverwriteGranted = isOverwriteAllChecked || overwriteDecisionCache.get(`${failure.obj.type}:${failure.obj.id}`) === true;
+
+ // Conflicts wihtout overwrite granted are skipped
+ if (!isOverwriteGranted && failure.error.type === 'conflict') {
+ return;
+ }
+
+ // Replace references if user chose a new reference
+ if (failure.error.type === 'missing_references') {
+ const objReplaceReferences = replaceReferencesCache.get(`${failure.obj.type}:${failure.obj.id}`) || [];
+ const indexPatternRefs = failure.error.references.filter(obj => obj.type === 'index-pattern');
+ for (const reference of indexPatternRefs) {
+ for (const unmatchedReference of unmatchedReferences) {
+ const hasNewValue = !!unmatchedReference.newIndexPatternId;
+ const matchesIndexPatternId = unmatchedReference.existingIndexPatternId === reference.id;
+ if (!hasNewValue || !matchesIndexPatternId) {
+ continue;
+ }
+ objReplaceReferences.push({
+ type: 'index-pattern',
+ from: unmatchedReference.existingIndexPatternId,
+ to: unmatchedReference.newIndexPatternId,
+ });
+ }
+ }
+ replaceReferencesCache.set(`${failure.obj.type}:${failure.obj.id}`, objReplaceReferences);
+ // Skip if nothing to replace, the UI option selected would be --Skip Import--
+ if (objReplaceReferences.length === 0) {
+ return;
+ }
+ }
+
+ return {
+ id: failure.obj.id,
+ type: failure.obj.type,
+ overwrite: isOverwriteAllChecked || overwriteDecisionCache.get(`${failure.obj.type}:${failure.obj.id}`) === true,
+ replaceReferences: replaceReferencesCache.get(`${failure.obj.type}:${failure.obj.id}`) || [],
+ };
+}
+
+export async function resolveImportErrors({ getConflictResolutions, state }) {
+ const overwriteDecisionCache = new Map();
+ const replaceReferencesCache = new Map();
+ let { importCount: successImportCount, failedImports: importFailures = [] } = state;
+ const { file, isOverwriteAllChecked } = state;
+
+ const doesntHaveOverwriteDecision = ({ obj }) => {
+ return !overwriteDecisionCache.has(`${obj.type}:${obj.id}`);
+ };
+ const getOverwriteDecision = ({ obj }) => {
+ return overwriteDecisionCache.get(`${obj.type}:${obj.id}`);
+ };
+ const callMapImportFailure = (failure) => {
+ return mapImportFailureToRetryObject({ failure, overwriteDecisionCache, replaceReferencesCache, state });
+ };
+ const isNotSkipped = (failure) => {
+ return (failure.error.type !== 'conflict' && failure.error.type !== 'missing_references') ||
+ getOverwriteDecision(failure);
+ };
+
+ // Loop until all issues are resolved
+ while (importFailures.some(failure => ['conflict', 'missing_references'].includes(failure.error.type))) {
+ // Ask for overwrites
+ if (!isOverwriteAllChecked) {
+ const result = await getConflictResolutions(
+ importFailures
+ .filter(({ error }) => error.type === 'conflict')
+ .filter(doesntHaveOverwriteDecision)
+ .map(({ obj }) => obj)
+ );
+ for (const key of Object.keys(result)) {
+ overwriteDecisionCache.set(key, result[key]);
+ }
+ }
+
+ // Build retries array
+ const retries = importFailures
+ .map(callMapImportFailure)
+ .filter(obj => !!obj);
+ for (const { error, obj } of importFailures) {
+ if (error.type !== 'missing_references') {
+ continue;
+ }
+ if (!retries.some(retryObj => retryObj.type === obj.type && retryObj.id === obj.id)) {
+ continue;
+ }
+ for (const { type, id } of error.blocking || []) {
+ retries.push({ type, id });
+ }
+ }
+
+ // Scenario where everything is skipped and nothing to retry
+ if (retries.length === 0) {
+ // Cancelled overwrites aren't failures anymore
+ importFailures = importFailures.filter(isNotSkipped);
+ break;
+ }
+
+ // Call API
+ const response = await callResolveImportErrorsApi(file, retries);
+ successImportCount += response.successCount;
+ importFailures = [];
+ for (const { error, ...obj } of response.errors || []) {
+ importFailures.push({ error, obj });
+ }
+ }
+
+ return {
+ status: 'success',
+ importCount: successImportCount,
+ failedImports: importFailures,
+ };
+}
diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js
index a84a805b2f1b94..07acfd49fbeeeb 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js
@@ -233,7 +233,11 @@ function VisEditor(
return !vis.hasInspector || !vis.hasInspector();
},
run() {
- vis.openInspector().bindToAngularScope($scope);
+ const inspectorSession = vis.openInspector();
+ // Close the inspector if this scope is destroyed (e.g. because the user navigates away).
+ const removeWatch = $scope.$on('$destroy', () => inspectorSession.close());
+ // Remove that watch in case the user closes the inspector session herself.
+ inspectorSession.onClose.finally(removeWatch);
},
tooltip() {
if (!vis.hasInspector || !vis.hasInspector()) {
diff --git a/src/legacy/core_plugins/markdown_vis/public/markdown_fn.js b/src/legacy/core_plugins/markdown_vis/public/markdown_fn.js
index 4cf7bd6af3cfa7..c30047713d909d 100644
--- a/src/legacy/core_plugins/markdown_vis/public/markdown_fn.js
+++ b/src/legacy/core_plugins/markdown_vis/public/markdown_fn.js
@@ -21,7 +21,7 @@ import { functionsRegistry } from 'plugins/interpreter/registries';
import { i18n } from '@kbn/i18n';
export const kibanaMarkdown = () => ({
- name: 'kibana_markdown',
+ name: 'markdownVis',
type: 'render',
context: {
types: [],
@@ -30,20 +30,28 @@ export const kibanaMarkdown = () => ({
defaultMessage: 'Markdown visualization'
}),
args: {
- visConfig: {
- types: ['string'],
- default: '"{}"',
+ markdown: {
+ type: ['string'],
+ aliases: ['_'],
+ required: true,
+ },
+ fontSize: {
+ types: ['number'],
+ default: 12,
+ },
+ openLinksInNewTab: {
+ types: ['boolean'],
+ default: false,
}
},
fn(context, args) {
- const params = JSON.parse(args.visConfig);
return {
type: 'render',
as: 'visualization',
value: {
visType: 'markdown',
visConfig: {
- ...params,
+ ...args,
},
}
};
diff --git a/src/legacy/core_plugins/markdown_vis/public/markdown_fn.test.js b/src/legacy/core_plugins/markdown_vis/public/markdown_fn.test.js
index 185db7cb995f12..18652aee65c20e 100644
--- a/src/legacy/core_plugins/markdown_vis/public/markdown_fn.test.js
+++ b/src/legacy/core_plugins/markdown_vis/public/markdown_fn.test.js
@@ -22,14 +22,14 @@ import { kibanaMarkdown } from './markdown_fn';
describe('interpreter/functions#markdown', () => {
const fn = functionWrapper(kibanaMarkdown);
- const visConfig = {
+ const args = {
fontSize: 12,
openLinksInNewTab: true,
markdown: '## hello _markdown_',
};
it('returns an object with the correct structure', () => {
- const actual = fn(undefined, { visConfig: JSON.stringify(visConfig) });
+ const actual = fn(undefined, args);
expect(actual).toMatchSnapshot();
});
});
diff --git a/src/legacy/core_plugins/metrics/common/field_types.js b/src/legacy/core_plugins/metrics/common/field_types.js
new file mode 100644
index 00000000000000..f5323f1542a315
--- /dev/null
+++ b/src/legacy/core_plugins/metrics/common/field_types.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const FIELD_TYPES = {
+ BOOLEAN: 'boolean',
+ DATE: 'date',
+ GEO: 'geo_point',
+ NUMBER: 'number',
+ STRING: 'string',
+};
diff --git a/src/legacy/core_plugins/metrics/public/components/aggs/static.js b/src/legacy/core_plugins/metrics/public/components/aggs/static.js
index d38fa6f75861d5..8d33aed3372dd6 100644
--- a/src/legacy/core_plugins/metrics/public/components/aggs/static.js
+++ b/src/legacy/core_plugins/metrics/public/components/aggs/static.js
@@ -42,7 +42,8 @@ export const Static = props => {
const defaults = {
numerator: '*',
denominator: '*',
- metric_agg: 'count'
+ metric_agg: 'count',
+ value: '',
};
const model = { ...defaults, ...props.model };
@@ -82,7 +83,7 @@ export const Static = props => {
>
diff --git a/src/legacy/core_plugins/metrics/public/components/splits/__snapshots__/terms.test.js.snap b/src/legacy/core_plugins/metrics/public/components/splits/__snapshots__/terms.test.js.snap
new file mode 100644
index 00000000000000..611dbbdbe00825
--- /dev/null
+++ b/src/legacy/core_plugins/metrics/public/components/splits/__snapshots__/terms.test.js.snap
@@ -0,0 +1,221 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js should render and match a snapshot 1`] = `
+
+
+
+
+ }
+ labelType="label"
+ >
+
+
+
+
+
+ }
+ labelType="label"
+ >
+
+
+
+
+
+
+
+ }
+ labelType="label"
+ >
+
+
+
+
+
+ }
+ labelType="label"
+ >
+
+
+
+
+
+
+
+ }
+ labelType="label"
+ >
+
+
+
+
+
+ }
+ labelType="label"
+ >
+
+
+
+
+
+ }
+ labelType="label"
+ >
+
+
+
+
+
+`;
diff --git a/src/legacy/core_plugins/metrics/public/components/splits/terms.js b/src/legacy/core_plugins/metrics/public/components/splits/terms.js
index f152c266dc4f60..295ff3a8a8a924 100644
--- a/src/legacy/core_plugins/metrics/public/components/splits/terms.js
+++ b/src/legacy/core_plugins/metrics/public/components/splits/terms.js
@@ -19,21 +19,23 @@
import PropTypes from 'prop-types';
import React from 'react';
+import { get, find } from 'lodash';
import GroupBySelect from './group_by_select';
import createTextHandler from '../lib/create_text_handler';
import createSelectHandler from '../lib/create_select_handler';
import FieldSelect from '../aggs/field_select';
import MetricSelect from '../aggs/metric_select';
-import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldNumber, EuiComboBox, EuiSpacer } from '@elastic/eui';
+import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldNumber, EuiComboBox, EuiFieldText } from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
+import { FIELD_TYPES } from '../../../common/field_types';
-const SplitByTermsUi = props => {
+const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' };
+
+export const SplitByTermsUI = ({ onChange, indexPattern, intl, model: seriesModel, fields }) => {
const htmlId = htmlIdGenerator();
- const handleTextChange = createTextHandler(props.onChange);
- const handleSelectChange = createSelectHandler(props.onChange);
- const { indexPattern, intl } = props;
- const defaults = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' };
- const model = { ...defaults, ...props.model };
+ const handleTextChange = createTextHandler(onChange);
+ const handleSelectChange = createSelectHandler(onChange);
+ const model = { ...DEFAULTS, ...seriesModel };
const { metrics } = model;
const defaultCount = {
value: '_count',
@@ -57,10 +59,12 @@ const SplitByTermsUi = props => {
const selectedDirectionOption = dirOptions.find(option => {
return model.terms_direction === option.value;
});
+ const selectedField = find(fields[indexPattern], ({ name }) => name === model.terms_field);
+ const selectedFieldType = get(selectedField, 'type');
return (
-
+
{
indexPattern={indexPattern}
onChange={handleSelectChange('terms_field')}
value={model.terms_field}
- fields={props.fields}
+ fields={fields}
/>
-
+ {selectedFieldType === FIELD_TYPES.STRING && (
+
+
+ )}
+ >
+
+
+
+
+ )}
+ >
+
+
+
+
+ )}
-
+
{
);
};
-SplitByTermsUi.propTypes = {
+SplitByTermsUI.propTypes = {
+ intl: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
indexPattern: PropTypes.string,
fields: PropTypes.object
};
-export const SplitByTerms = injectI18n(SplitByTermsUi);
+export const SplitByTerms = injectI18n(SplitByTermsUI);
diff --git a/src/legacy/core_plugins/metrics/public/components/splits/terms.test.js b/src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
new file mode 100644
index 00000000000000..4adbeab57dce66
--- /dev/null
+++ b/src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
@@ -0,0 +1,68 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { SplitByTermsUI } from './terms';
+
+jest.mock('@elastic/eui', () => ({
+ htmlIdGenerator: jest.fn(() => () => '42'),
+ EuiFlexGroup: require.requireActual('@elastic/eui').EuiFlexGroup,
+ EuiFlexItem: require.requireActual('@elastic/eui').EuiFlexItem,
+ EuiFormRow: require.requireActual('@elastic/eui').EuiFormRow,
+ EuiFieldNumber: require.requireActual('@elastic/eui').EuiFieldNumber,
+ EuiComboBox: require.requireActual('@elastic/eui').EuiComboBox,
+ EuiFieldText: require.requireActual('@elastic/eui').EuiFieldText,
+}));
+
+describe('src/legacy/core_plugins/metrics/public/components/splits/terms.test.js', () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ intl: {
+ formatMessage: jest.fn(),
+ },
+ model: {
+ terms_field: 'OriginCityName'
+ },
+ onChange: jest.fn(),
+ indexPattern: 'kibana_sample_data_flights',
+ fields: {
+ 'kibana_sample_data_flights': [
+ {
+ aggregatable: true,
+ name: 'OriginCityName',
+ readFromDocValues: true,
+ searchable: true,
+ type: 'string'
+ }
+ ]
+ },
+ };
+ });
+
+ describe(' ', () => {
+ test('should render and match a snapshot', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/legacy/core_plugins/metrics/public/components/vis_editor.js b/src/legacy/core_plugins/metrics/public/components/vis_editor.js
index c1701552a89368..0632ef43a8114b 100644
--- a/src/legacy/core_plugins/metrics/public/components/vis_editor.js
+++ b/src/legacy/core_plugins/metrics/public/components/vis_editor.js
@@ -21,12 +21,14 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import * as Rx from 'rxjs';
import { share } from 'rxjs/operators';
+import { isEqual, isEmpty } from 'lodash';
import VisEditorVisualization from './vis_editor_visualization';
import Visualization from './visualization';
import VisPicker from './vis_picker';
import PanelConfig from './panel_config';
import brushHandler from '../lib/create_brush_handler';
-import { fetchIndexPatternFields } from '../lib/fetch_fields';
+import { fetchFields } from '../lib/fetch_fields';
+import { extractIndexPatterns } from '../lib/extract_index_patterns';
class VisEditor extends Component {
constructor(props) {
@@ -37,7 +39,8 @@ class VisEditor extends Component {
model: props.visParams,
dirty: false,
autoApply: true,
- visFields: props.visFields
+ visFields: props.visFields,
+ extractedIndexPatterns: [''],
};
this.onBrush = brushHandler(props.vis.API.timeFilter);
this.visDataSubject = new Rx.Subject();
@@ -52,23 +55,45 @@ class VisEditor extends Component {
return this.props.config.get(...args);
};
- handleUiState = (field, value) => {
+ handleUiState = (field, value) => {
this.props.vis.uiStateVal(field, value);
};
handleChange = async (partialModel) => {
- const nextModel = { ...this.state.model, ...partialModel };
+ if (isEmpty(partialModel)) {
+ return;
+ }
+ const hasTypeChanged = partialModel.type && this.state.model.type !== partialModel.type;
+ const nextModel = {
+ ...this.state.model,
+ ...partialModel,
+ };
+ let dirty = true;
+
this.props.vis.params = nextModel;
- if (this.state.autoApply) {
+
+ if (this.state.autoApply || hasTypeChanged) {
this.props.vis.updateState();
+
+ dirty = false;
}
+
+ if (this.props.isEditorMode) {
+ const { params, fields } = this.props.vis;
+ const extractedIndexPatterns = extractIndexPatterns(params, fields);
+
+ if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) {
+ fetchFields(extractedIndexPatterns)
+ .then(visFields => this.setState({
+ visFields,
+ extractedIndexPatterns,
+ }));
+ }
+ }
+
this.setState({
+ dirty,
model: nextModel,
- dirty: !this.state.autoApply
- });
- const { params, fields } = this.props.vis;
- fetchIndexPatternFields(params, fields).then(visFields => {
- this.setState({ visFields });
});
};
@@ -109,7 +134,7 @@ class VisEditor extends Component {
return (
-
+
{
- const savedObjectsClient = chrome.getSavedObjectsClient();
- const indexPattern = await savedObjectsClient.get('index-pattern', config.get('defaultIndex'));
- this.vis.params.default_index_pattern = indexPattern.attributes.title;
- };
-
- async render(params) {
- const Component = this.vis.type.editorConfig.component;
-
- await this.setDefaultIndexPattern();
- const visFields = await fetchIndexPatternFields(this.vis.params, this.vis.fields);
-
- render(
-
- {}}
- isEditorMode={true}
- appState={params.appState}
- />
- ,
- this.el);
- }
-
- resize() {}
-
- destroy() {
- unmountComponentAtNode(this.el);
- }
- }
-
- return {
- name: 'react_editor',
- handler: ReactEditorController
- };
-}
-
-export { ReactEditorControllerProvider };
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { render, unmountComponentAtNode } from 'react-dom';
+import { I18nContext } from 'ui/i18n';
+import chrome from 'ui/chrome';
+import { fetchIndexPatternFields } from '../lib/fetch_fields';
+
+function ReactEditorControllerProvider(Private, config) {
+ class ReactEditorController {
+ constructor(el, savedObj) {
+ this.el = el;
+
+ this.state = {
+ savedObj: savedObj,
+ vis: savedObj.vis,
+ isLoaded: false,
+ };
+ }
+
+ fetchDefaultIndexPattern = async () => {
+ const savedObjectsClient = chrome.getSavedObjectsClient();
+ const indexPattern = await savedObjectsClient.get('index-pattern', config.get('defaultIndex'));
+
+ return indexPattern.attributes.title;
+ };
+
+ fetchDefaultParams = async () => {
+ this.state.vis.params.default_index_pattern = await this.fetchDefaultIndexPattern();
+ this.state.vis.fields = await fetchIndexPatternFields(this.state.vis);
+
+ this.state.isLoaded = true;
+ };
+
+ getComponent = () => {
+ return this.state.vis.type.editorConfig.component;
+ };
+
+ async render(params) {
+ const Component = this.getComponent();
+
+ !this.state.isLoaded && await this.fetchDefaultParams();
+
+ render(
+
+ {}}
+ isEditorMode={true}
+ appState={params.appState}
+ />
+ ,
+ this.el);
+ }
+
+ destroy() {
+ unmountComponentAtNode(this.el);
+ }
+ }
+
+ return {
+ name: 'react_editor',
+ handler: ReactEditorController,
+ };
+}
+
+export { ReactEditorControllerProvider };
diff --git a/src/legacy/core_plugins/metrics/public/lib/extract_index_patterns.js b/src/legacy/core_plugins/metrics/public/lib/extract_index_patterns.js
index 7f800f03492362..6aedc9ebe2c063 100644
--- a/src/legacy/core_plugins/metrics/public/lib/extract_index_patterns.js
+++ b/src/legacy/core_plugins/metrics/public/lib/extract_index_patterns.js
@@ -16,8 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-
import { uniq } from 'lodash';
+
export function extractIndexPatterns(params, fetchedFields) {
const patternsToFetch = [];
@@ -41,6 +41,9 @@ export function extractIndexPatterns(params, fetchedFields) {
});
}
- return uniq(patternsToFetch);
+ if (patternsToFetch.length === 0) {
+ patternsToFetch.push('');
+ }
+ return uniq(patternsToFetch).sort();
}
diff --git a/src/legacy/core_plugins/metrics/public/lib/fetch_fields.js b/src/legacy/core_plugins/metrics/public/lib/fetch_fields.js
index 7caeb1b4d46a16..de60b5ac0e7cd8 100644
--- a/src/legacy/core_plugins/metrics/public/lib/fetch_fields.js
+++ b/src/legacy/core_plugins/metrics/public/lib/fetch_fields.js
@@ -30,27 +30,28 @@ export async function fetchFields(indexPatterns = ['*']) {
pathname: '/api/metrics/fields',
query: {
index: pattern,
- }
+ },
});
}));
const fields = patterns.reduce((cumulatedFields, currentPattern, index) => {
return {
...cumulatedFields,
- [currentPattern]: indexFields[index]
+ [currentPattern]: indexFields[index],
};
}, {});
return fields;
- } catch(error) {
+ } catch (error) {
toastNotifications.addDanger({
title: i18n.translate('tsvb.fetchFields.loadIndexPatternFieldsErrorMessage', {
- defaultMessage: 'Unable to load index_pattern fields'
+ defaultMessage: 'Unable to load index_pattern fields',
}),
text: error.message,
});
}
}
-export async function fetchIndexPatternFields(params, fields) {
+export async function fetchIndexPatternFields({ params, fields = {} }) {
const indexPatterns = extractIndexPatterns(params, fields);
+
return await fetchFields(indexPatterns);
}
diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_terms.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_terms.js
index 54f1105e643e28..6ee9b801cf6fab 100644
--- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_terms.js
+++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/split_by_terms.js
@@ -17,7 +17,7 @@
* under the License.
*/
-import _ from 'lodash';
+import { set } from 'lodash';
import basicAggs from '../../../../../common/basic_aggs';
import getBucketsPath from '../../helpers/get_buckets_path';
import bucketTransform from '../../helpers/bucket_transform';
@@ -26,20 +26,26 @@ export default function splitByTerm(req, panel, series) {
return next => doc => {
if (series.split_mode === 'terms' && series.terms_field) {
const direction = series.terms_direction || 'desc';
- _.set(doc, `aggs.${series.id}.terms.field`, series.terms_field);
- _.set(doc, `aggs.${series.id}.terms.size`, series.terms_size);
const metric = series.metrics.find(item => item.id === series.terms_order_by);
+ set(doc, `aggs.${series.id}.terms.field`, series.terms_field);
+ set(doc, `aggs.${series.id}.terms.size`, series.terms_size);
+ if (series.terms_include) {
+ set(doc, `aggs.${series.id}.terms.include`, series.terms_include);
+ }
+ if (series.terms_exclude) {
+ set(doc, `aggs.${series.id}.terms.exclude`, series.terms_exclude);
+ }
if (metric && metric.type !== 'count' && ~basicAggs.indexOf(metric.type)) {
const sortAggKey = `${series.terms_order_by}-SORT`;
const fn = bucketTransform[metric.type];
const bucketPath = getBucketsPath(series.terms_order_by, series.metrics)
.replace(series.terms_order_by, sortAggKey);
- _.set(doc, `aggs.${series.id}.terms.order`, { [bucketPath]: direction });
- _.set(doc, `aggs.${series.id}.aggs`, { [sortAggKey]: fn(metric) });
+ set(doc, `aggs.${series.id}.terms.order`, { [bucketPath]: direction });
+ set(doc, `aggs.${series.id}.aggs`, { [sortAggKey]: fn(metric) });
} else if (['_key', '_count'].includes(series.terms_order_by)) {
- _.set(doc, `aggs.${series.id}.terms.order`, { [series.terms_order_by]: direction });
+ set(doc, `aggs.${series.id}.terms.order`, { [series.terms_order_by]: direction });
} else {
- _.set(doc, `aggs.${series.id}.terms.order`, { _count: direction });
+ set(doc, `aggs.${series.id}.terms.order`, { _count: direction });
}
}
return next(doc);
diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/time_shift.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/time_shift.js
index 1e40a72e34fc8d..0712a8ceec7407 100644
--- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/time_shift.js
+++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/time_shift.js
@@ -23,16 +23,23 @@ export default function timeShift(resp, panel, series) {
return next => results => {
if (/^([+-]?[\d]+)([shmdwMy]|ms)$/.test(series.offset_time)) {
const matches = series.offset_time.match(/^([+-]?[\d]+)([shmdwMy]|ms)$/);
+
if (matches) {
- const offsetValue = matches[1];
+ const offsetValue = Number(matches[1]);
const offsetUnit = matches[2];
+ const offset = moment.duration(offsetValue, offsetUnit).valueOf();
+
results.forEach(item => {
if (_.startsWith(item.id, series.id)) {
- item.data = item.data.map(row => [moment(row[0]).add(offsetValue, offsetUnit).valueOf(), row[1]]);
+ item.data = item.data.map(([time, value]) => [
+ time + offset,
+ value
+ ]);
}
});
}
}
+
return next(results);
};
}
diff --git a/src/legacy/server/saved_objects/import/collect_saved_objects.test.ts b/src/legacy/server/saved_objects/import/collect_saved_objects.test.ts
index 80a212066d8eed..8ccd29ef208382 100644
--- a/src/legacy/server/saved_objects/import/collect_saved_objects.test.ts
+++ b/src/legacy/server/saved_objects/import/collect_saved_objects.test.ts
@@ -43,6 +43,7 @@ describe('collectSavedObjects()', () => {
Array [
Object {
"foo": true,
+ "migrationVersion": Object {},
},
]
`);
@@ -60,6 +61,7 @@ Array [
Array [
Object {
"foo": true,
+ "migrationVersion": Object {},
},
]
`);
diff --git a/src/legacy/server/saved_objects/import/collect_saved_objects.ts b/src/legacy/server/saved_objects/import/collect_saved_objects.ts
index 39c865f4b13dcd..cf885c583f714a 100644
--- a/src/legacy/server/saved_objects/import/collect_saved_objects.ts
+++ b/src/legacy/server/saved_objects/import/collect_saved_objects.ts
@@ -44,6 +44,10 @@ export async function collectSavedObjects(
createFilterStream(obj => !!obj),
createLimitStream(objectLimit),
createFilterStream(obj => (filter ? filter(obj) : true)),
+ createMapStream((obj: SavedObject) => {
+ // Ensure migrations execute on every saved object
+ return Object.assign({ migrationVersion: {} }, obj);
+ }),
createConcatStream([]),
])) as SavedObject[];
}
diff --git a/src/legacy/server/saved_objects/import/import_saved_objects.test.ts b/src/legacy/server/saved_objects/import/import_saved_objects.test.ts
index 95aff38f27e264..a0f2c4153876c2 100644
--- a/src/legacy/server/saved_objects/import/import_saved_objects.test.ts
+++ b/src/legacy/server/saved_objects/import/import_saved_objects.test.ts
@@ -124,6 +124,7 @@ Object {
"title": "My Index Pattern",
},
"id": "1",
+ "migrationVersion": Object {},
"references": Array [],
"type": "index-pattern",
},
@@ -132,6 +133,7 @@ Object {
"title": "My Search",
},
"id": "2",
+ "migrationVersion": Object {},
"references": Array [],
"type": "search",
},
@@ -140,6 +142,7 @@ Object {
"title": "My Visualization",
},
"id": "3",
+ "migrationVersion": Object {},
"references": Array [],
"type": "visualization",
},
@@ -148,6 +151,7 @@ Object {
"title": "My Dashboard",
},
"id": "4",
+ "migrationVersion": Object {},
"references": Array [],
"type": "dashboard",
},
@@ -200,6 +204,7 @@ Object {
"title": "My Index Pattern",
},
"id": "1",
+ "migrationVersion": Object {},
"references": Array [],
"type": "index-pattern",
},
@@ -208,6 +213,7 @@ Object {
"title": "My Search",
},
"id": "2",
+ "migrationVersion": Object {},
"references": Array [],
"type": "search",
},
@@ -216,6 +222,7 @@ Object {
"title": "My Visualization",
},
"id": "3",
+ "migrationVersion": Object {},
"references": Array [],
"type": "visualization",
},
@@ -224,6 +231,7 @@ Object {
"title": "My Dashboard",
},
"id": "4",
+ "migrationVersion": Object {},
"references": Array [],
"type": "dashboard",
},
diff --git a/src/legacy/server/saved_objects/import/resolve_import_errors.test.ts b/src/legacy/server/saved_objects/import/resolve_import_errors.test.ts
index 4885630dfb72df..05351fab828af9 100644
--- a/src/legacy/server/saved_objects/import/resolve_import_errors.test.ts
+++ b/src/legacy/server/saved_objects/import/resolve_import_errors.test.ts
@@ -141,6 +141,7 @@ Object {
"title": "My Visualization",
},
"id": "3",
+ "migrationVersion": Object {},
"references": Array [],
"type": "visualization",
},
@@ -196,6 +197,7 @@ Object {
"title": "My Index Pattern",
},
"id": "1",
+ "migrationVersion": Object {},
"references": Array [],
"type": "index-pattern",
},
@@ -260,6 +262,7 @@ Object {
"title": "My Dashboard",
},
"id": "4",
+ "migrationVersion": Object {},
"references": Array [
Object {
"id": "13",
diff --git a/src/legacy/server/saved_objects/routes/import.test.ts b/src/legacy/server/saved_objects/routes/import.test.ts
index a907de8d7c691e..8da2ccccc80c65 100644
--- a/src/legacy/server/saved_objects/routes/import.test.ts
+++ b/src/legacy/server/saved_objects/routes/import.test.ts
@@ -77,6 +77,47 @@ describe('POST /api/saved_objects/_import', () => {
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
});
+ test('defaults migrationVersion to empty object', async () => {
+ const request = {
+ method: 'POST',
+ url: '/api/saved_objects/_import',
+ payload: [
+ '--EXAMPLE',
+ 'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
+ 'Content-Type: application/ndjson',
+ '',
+ '{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
+ '--EXAMPLE--',
+ ].join('\r\n'),
+ headers: {
+ 'content-Type': 'multipart/form-data; boundary=EXAMPLE',
+ },
+ };
+ savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
+ savedObjectsClient.bulkCreate.mockResolvedValueOnce({
+ saved_objects: [
+ {
+ type: 'index-pattern',
+ id: 'my-pattern',
+ attributes: {
+ title: 'my-pattern-*',
+ },
+ },
+ ],
+ });
+ const { payload, statusCode } = await server.inject(request);
+ const response = JSON.parse(payload);
+ expect(statusCode).toBe(200);
+ expect(response).toEqual({
+ success: true,
+ successCount: 1,
+ });
+ expect(savedObjectsClient.bulkCreate.mock.calls).toHaveLength(1);
+ const firstBulkCreateCallArray = savedObjectsClient.bulkCreate.mock.calls[0][0];
+ expect(firstBulkCreateCallArray).toHaveLength(1);
+ expect(firstBulkCreateCallArray[0].migrationVersion).toEqual({});
+ });
+
test('imports an index pattern and dashboard', async () => {
// NOTE: changes to this scenario should be reflected in the docs
const request = {
diff --git a/src/legacy/server/saved_objects/routes/index.ts b/src/legacy/server/saved_objects/routes/index.ts
index 18b400400714a3..4c3d8c41631553 100644
--- a/src/legacy/server/saved_objects/routes/index.ts
+++ b/src/legacy/server/saved_objects/routes/index.ts
@@ -24,6 +24,7 @@ export { createDeleteRoute } from './delete';
export { createFindRoute } from './find';
export { createGetRoute } from './get';
export { createImportRoute } from './import';
+export { createLogLegacyImportRoute } from './log_legacy_import';
export { createResolveImportErrorsRoute } from './resolve_import_errors';
export { createUpdateRoute } from './update';
export { createExportRoute } from './export';
diff --git a/src/legacy/server/saved_objects/routes/log_legacy_import.ts b/src/legacy/server/saved_objects/routes/log_legacy_import.ts
new file mode 100644
index 00000000000000..038c03d30e030e
--- /dev/null
+++ b/src/legacy/server/saved_objects/routes/log_legacy_import.ts
@@ -0,0 +1,34 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import Hapi from 'hapi';
+
+export const createLogLegacyImportRoute = () => ({
+ path: '/api/saved_objects/_log_legacy_import',
+ method: 'POST',
+ options: {
+ handler(request: Hapi.Request) {
+ request.server.log(
+ ['warning'],
+ 'Importing saved objects from a .json file has been deprecated'
+ );
+ return { success: true };
+ },
+ },
+});
diff --git a/src/legacy/server/saved_objects/routes/resolve_import_errors.test.ts b/src/legacy/server/saved_objects/routes/resolve_import_errors.test.ts
index 0ab2965655b0b8..60de947759df57 100644
--- a/src/legacy/server/saved_objects/routes/resolve_import_errors.test.ts
+++ b/src/legacy/server/saved_objects/routes/resolve_import_errors.test.ts
@@ -77,7 +77,48 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
});
- test('retries importin a dashboard', async () => {
+ test('defaults migrationVersion to empty object', async () => {
+ const request = {
+ method: 'POST',
+ url: '/api/saved_objects/_resolve_import_errors',
+ payload: [
+ '--EXAMPLE',
+ 'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
+ 'Content-Type: application/ndjson',
+ '',
+ '{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
+ '--EXAMPLE',
+ 'Content-Disposition: form-data; name="retries"',
+ '',
+ '[{"type":"dashboard","id":"my-dashboard"}]',
+ '--EXAMPLE--',
+ ].join('\r\n'),
+ headers: {
+ 'content-Type': 'multipart/form-data; boundary=EXAMPLE',
+ },
+ };
+ savedObjectsClient.bulkCreate.mockResolvedValueOnce({
+ saved_objects: [
+ {
+ type: 'dashboard',
+ id: 'my-dashboard',
+ attributes: {
+ title: 'Look at my dashboard',
+ },
+ },
+ ],
+ });
+ const { payload, statusCode } = await server.inject(request);
+ const response = JSON.parse(payload);
+ expect(statusCode).toBe(200);
+ expect(response).toEqual({ success: true, successCount: 1 });
+ expect(savedObjectsClient.bulkCreate.mock.calls).toHaveLength(1);
+ const firstBulkCreateCallArray = savedObjectsClient.bulkCreate.mock.calls[0][0];
+ expect(firstBulkCreateCallArray).toHaveLength(1);
+ expect(firstBulkCreateCallArray[0].migrationVersion).toEqual({});
+ });
+
+ test('retries importing a dashboard', async () => {
// NOTE: changes to this scenario should be reflected in the docs
const request = {
method: 'POST',
@@ -123,6 +164,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
"title": "Look at my dashboard",
},
"id": "my-dashboard",
+ "migrationVersion": Object {},
"type": "dashboard",
},
],
@@ -185,6 +227,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
"title": "Look at my dashboard",
},
"id": "my-dashboard",
+ "migrationVersion": Object {},
"type": "dashboard",
},
],
@@ -266,6 +309,7 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
"title": "Look at my visualization",
},
"id": "my-vis",
+ "migrationVersion": Object {},
"references": Array [
Object {
"id": "existing",
diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js
index f53dbb0303b9dc..3e35b89d7e4bf1 100644
--- a/src/legacy/server/saved_objects/saved_objects_mixin.js
+++ b/src/legacy/server/saved_objects/saved_objects_mixin.js
@@ -38,6 +38,7 @@ import {
createExportRoute,
createImportRoute,
createResolveImportErrorsRoute,
+ createLogLegacyImportRoute,
} from './routes';
export function savedObjectsMixin(kbnServer, server) {
@@ -71,6 +72,7 @@ export function savedObjectsMixin(kbnServer, server) {
server.route(createExportRoute(prereqs, server));
server.route(createImportRoute(prereqs, server));
server.route(createResolveImportErrorsRoute(prereqs, server));
+ server.route(createLogLegacyImportRoute());
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
const serializer = new SavedObjectsSerializer(schema);
diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js
index d45ad752111324..11a50a313323d0 100644
--- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js
+++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js
@@ -110,9 +110,9 @@ describe('Saved Objects Mixin', () => {
});
describe('Routes', () => {
- it('should create 10 routes', () => {
+ it('should create 11 routes', () => {
savedObjectsMixin(mockKbnServer, mockServer);
- expect(mockServer.route).toHaveBeenCalledTimes(10);
+ expect(mockServer.route).toHaveBeenCalledTimes(11);
});
it('should add POST /api/saved_objects/_bulk_create', () => {
savedObjectsMixin(mockKbnServer, mockServer);
@@ -177,6 +177,12 @@ describe('Saved Objects Mixin', () => {
})
);
});
+ it('should add POST /api/saved_objects/_log_legacy_import', () => {
+ savedObjectsMixin(mockKbnServer, mockServer);
+ expect(mockServer.route).toHaveBeenCalledWith(
+ expect.objectContaining({ path: '/api/saved_objects/_log_legacy_import', method: 'POST' })
+ );
+ });
});
describe('Saved object service', () => {
diff --git a/src/legacy/ui/public/agg_types/buckets/date_histogram.js b/src/legacy/ui/public/agg_types/buckets/date_histogram.js
index 923169f2b4da0b..ea326c902ad53e 100644
--- a/src/legacy/ui/public/agg_types/buckets/date_histogram.js
+++ b/src/legacy/ui/public/agg_types/buckets/date_histogram.js
@@ -27,7 +27,7 @@ import { createFilterDateHistogram } from './create_filter/date_histogram';
import { intervalOptions } from './_interval_options';
import intervalTemplate from '../controls/time_interval.html';
import { timefilter } from '../../timefilter';
-import dropPartialTemplate from '../controls/drop_partials.html';
+import { DropPartialsParamEditor } from '../controls/drop_partials';
import { i18n } from '@kbn/i18n';
const config = chrome.getUiSettingsClient();
@@ -164,7 +164,7 @@ export const dateHistogramBucketAgg = new BucketAggType({
name: 'drop_partials',
default: false,
write: _.noop,
- editor: dropPartialTemplate,
+ editorComponent: DropPartialsParamEditor,
},
{
diff --git a/src/legacy/ui/public/agg_types/controls/drop_partials.html b/src/legacy/ui/public/agg_types/controls/drop_partials.html
deleted file mode 100644
index d1bff6a67ae025..00000000000000
--- a/src/legacy/ui/public/agg_types/controls/drop_partials.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/legacy/ui/public/agg_types/controls/drop_partials.tsx b/src/legacy/ui/public/agg_types/controls/drop_partials.tsx
new file mode 100644
index 00000000000000..189e0eaa24ce06
--- /dev/null
+++ b/src/legacy/ui/public/agg_types/controls/drop_partials.tsx
@@ -0,0 +1,55 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+
+import { EuiSpacer, EuiSwitch, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { AggParamEditorProps } from 'ui/vis/editors/default';
+
+function DropPartialsParamEditor({ agg, aggParam, value, setValue }: AggParamEditorProps) {
+ if (agg.params.field.name !== agg.getIndexPattern().timeFieldName) {
+ return null;
+ }
+
+ const content = i18n.translate('common.ui.aggTypes.dropPartialBucketsTooltip', {
+ defaultMessage:
+ "Remove buckets that span time outside the time range so the histogram doesn't start and end with incomplete buckets.",
+ });
+
+ const label = i18n.translate('common.ui.aggTypes.dropPartialBucketsLabel', {
+ defaultMessage: 'Drop partial buckets',
+ });
+
+ return (
+ <>
+
+ setValue(ev.target.checked)}
+ />
+
+
+ >
+ );
+}
+
+export { DropPartialsParamEditor };
diff --git a/src/legacy/ui/public/flyout/flyout_session.tsx b/src/legacy/ui/public/flyout/flyout_session.tsx
deleted file mode 100644
index 7111b9b04b0186..00000000000000
--- a/src/legacy/ui/public/flyout/flyout_session.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import React from 'react';
-
-import { EuiFlyout } from '@elastic/eui';
-import { EventEmitter } from 'events';
-import ReactDOM from 'react-dom';
-import { I18nContext } from 'ui/i18n';
-
-let activeSession: FlyoutSession | null = null;
-
-const CONTAINER_ID = 'flyout-container';
-
-function getOrCreateContainerElement() {
- let container = document.getElementById(CONTAINER_ID);
- if (!container) {
- container = document.createElement('div');
- container.id = CONTAINER_ID;
- document.body.appendChild(container);
- }
- return container;
-}
-
-/**
- * A FlyoutSession describes the session of one opened flyout panel. It offers
- * methods to close the flyout panel again. If you open a flyout panel you should make
- * sure you call {@link FlyoutSession#close} when it should be closed.
- * Since a flyout could also be closed without calling this method (e.g. because
- * the user closes it), you must listen to the "closed" event on this instance.
- * It will be emitted whenever the flyout will be closed and you should throw
- * away your reference to this instance whenever you receive that event.
- * @extends EventEmitter
- */
-class FlyoutSession extends EventEmitter {
- /**
- * Binds the current flyout session to an Angular scope, meaning this flyout
- * session will be closed as soon as the Angular scope gets destroyed.
- * @param {object} scope - An angular scope object to bind to.
- */
- public bindToAngularScope(scope: ng.IScope): void {
- const removeWatch = scope.$on('$destroy', () => this.close());
- this.on('closed', () => removeWatch());
- }
-
- /**
- * Closes the opened flyout as long as it's still the open one.
- * If this is not the active session anymore, this method won't do anything.
- * If this session was still active and a flyout was closed, the 'closed'
- * event will be emitted on this FlyoutSession instance.
- */
- public close(): void {
- if (activeSession === this) {
- const container = document.getElementById(CONTAINER_ID);
- if (container) {
- ReactDOM.unmountComponentAtNode(container);
- this.emit('closed');
- }
- }
- }
-}
-
-/**
- * Opens a flyout panel with the given component inside. You can use
- * {@link FlyoutSession#close} on the return value to close the flyout.
- *
- * @param flyoutChildren - Mounts the children inside a fly out panel
- * @return {FlyoutSession} The session instance for the opened flyout panel.
- */
-export function openFlyout(
- flyoutChildren: React.ReactNode,
- flyoutProps: {
- closeButtonAriaLabel?: string;
- onClose?: () => void;
- 'data-test-subj'?: string;
- } = {}
-): FlyoutSession {
- // If there is an active inspector session close it before opening a new one.
- if (activeSession) {
- activeSession.close();
- }
- const container = getOrCreateContainerElement();
- const session = (activeSession = new FlyoutSession());
- const onClose = () => {
- if (flyoutProps.onClose) {
- flyoutProps.onClose();
- }
- session.close();
- };
-
- ReactDOM.render(
-
-
- {flyoutChildren}
-
- ,
- container
- );
-
- return session;
-}
-
-export { FlyoutSession };
diff --git a/src/legacy/ui/public/inspector/inspector.test.js b/src/legacy/ui/public/inspector/inspector.test.js
index cc606311a31087..dbe84f9960bd73 100644
--- a/src/legacy/ui/public/inspector/inspector.test.js
+++ b/src/legacy/ui/public/inspector/inspector.test.js
@@ -28,6 +28,18 @@ jest.mock('./ui/inspector_panel', () => ({
}));
jest.mock('ui/i18n', () => ({ I18nContext: ({ children }) => children }));
+jest.mock('ui/new_platform', () => ({
+ getNewPlatform: () => ({
+ start: {
+ core: {
+ overlay: {
+ openFlyout: jest.fn(),
+ },
+ }
+ }
+ }),
+}));
+
import { viewRegistry } from './view_registry';
function setViews(views) {
@@ -52,60 +64,5 @@ describe('Inspector', () => {
setViews([]);
expect(() => Inspector.open({})).toThrow();
});
-
- describe('return value', () => {
- beforeEach(() => {
- setViews([{}]);
- });
-
- it('should be an object with a close function', () => {
- const session = Inspector.open({});
- expect(typeof session.close).toBe('function');
- });
-
- it('should emit the "closed" event if another inspector opens', () => {
- const session = Inspector.open({});
- const spy = jest.fn();
- session.on('closed', spy);
- Inspector.open({});
- expect(spy).toHaveBeenCalled();
- });
-
- it('should emit the "closed" event if you call close', () => {
- const session = Inspector.open({});
- const spy = jest.fn();
- session.on('closed', spy);
- session.close();
- expect(spy).toHaveBeenCalled();
- });
-
- it('can be bound to an angular scope', () => {
- const session = Inspector.open({});
- const spy = jest.fn();
- session.on('closed', spy);
- const scope = {
- $on: jest.fn(() => () => {})
- };
- session.bindToAngularScope(scope);
- expect(scope.$on).toHaveBeenCalled();
- const onCall = scope.$on.mock.calls[0];
- expect(onCall[0]).toBe('$destroy');
- expect(typeof onCall[1]).toBe('function');
- // Call $destroy callback, as angular would when the scope gets destroyed
- onCall[1]();
- expect(spy).toHaveBeenCalled();
- });
-
- it('will remove from angular scope when closed', () => {
- const session = Inspector.open({});
- const unwatchSpy = jest.fn();
- const scope = {
- $on: jest.fn(() => unwatchSpy)
- };
- session.bindToAngularScope(scope);
- session.close();
- expect(unwatchSpy).toHaveBeenCalled();
- });
- });
});
});
diff --git a/src/legacy/ui/public/inspector/inspector.tsx b/src/legacy/ui/public/inspector/inspector.tsx
index c0bae0a050f4ce..06163edb6e7367 100644
--- a/src/legacy/ui/public/inspector/inspector.tsx
+++ b/src/legacy/ui/public/inspector/inspector.tsx
@@ -19,7 +19,8 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { FlyoutSession, openFlyout } from 'ui/flyout';
+import { FlyoutRef } from '../../../../core/public';
+import { getNewPlatform } from '../new_platform';
import { Adapters } from './types';
import { InspectorPanel } from './ui/inspector_panel';
import { viewRegistry } from './view_registry';
@@ -49,7 +50,7 @@ interface InspectorOptions {
title?: string;
}
-export type InspectorSession = FlyoutSession;
+export type InspectorSession = FlyoutRef;
/**
* Opens the inspector panel for the given adapters and close any previously opened
@@ -72,10 +73,13 @@ function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSess
if an inspector can be shown.`);
}
- return openFlyout( , {
- 'data-test-subj': 'inspectorPanel',
- closeButtonAriaLabel: closeButtonLabel,
- });
+ return getNewPlatform().setup.core.overlays.openFlyout(
+ ,
+ {
+ 'data-test-subj': 'inspectorPanel',
+ closeButtonAriaLabel: closeButtonLabel,
+ }
+ );
}
const Inspector = {
diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts
index 086692a66261be..93d736bd8666ed 100644
--- a/src/legacy/ui/public/kfetch/kfetch.ts
+++ b/src/legacy/ui/public/kfetch/kfetch.ts
@@ -61,11 +61,14 @@ export async function kfetch(
});
return window.fetch(fullUrl, restOptions).then(async res => {
- const body = await getBodyAsJson(res);
- if (res.ok) {
- return body;
+ if (!res.ok) {
+ throw new KFetchError(res, await getBodyAsJson(res));
}
- throw new KFetchError(res, body);
+ const contentType = res.headers.get('content-type');
+ if (contentType && contentType.split(';')[0] === 'application/ndjson') {
+ return await getBodyAsBlob(res);
+ }
+ return await getBodyAsJson(res);
});
}
);
@@ -96,13 +99,25 @@ async function getBodyAsJson(res: Response) {
}
}
+async function getBodyAsBlob(res: Response) {
+ try {
+ return await res.blob();
+ } catch (e) {
+ return null;
+ }
+}
+
export function withDefaultOptions(options?: KFetchOptions): KFetchOptions {
return merge(
{
method: 'GET',
credentials: 'same-origin',
headers: {
- 'Content-Type': 'application/json',
+ ...(options && options.headers && options.headers.hasOwnProperty('Content-Type')
+ ? {}
+ : {
+ 'Content-Type': 'application/json',
+ }),
'kbn-version': metadata.version,
},
},
diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts
index 8a5029e9cda3c7..400ce8a4371a15 100644
--- a/src/legacy/ui/public/new_platform/new_platform.ts
+++ b/src/legacy/ui/public/new_platform/new_platform.ts
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-
import { CoreSetup } from '../../../../core/public';
const runtimeContext = {
diff --git a/src/legacy/ui/public/saved_objects/components/__snapshots__/saved_object_save_modal.test.js.snap b/src/legacy/ui/public/saved_objects/components/__snapshots__/saved_object_save_modal.test.tsx.snap
similarity index 100%
rename from src/legacy/ui/public/saved_objects/components/__snapshots__/saved_object_save_modal.test.js.snap
rename to src/legacy/ui/public/saved_objects/components/__snapshots__/saved_object_save_modal.test.tsx.snap
diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.test.js b/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.test.tsx
similarity index 90%
rename from src/legacy/ui/public/saved_objects/components/saved_object_save_modal.test.js
rename to src/legacy/ui/public/saved_objects/components/saved_object_save_modal.test.tsx
index 4e03c1c7e8b15d..65bd8e1d48e036 100644
--- a/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.test.js
+++ b/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.test.tsx
@@ -16,19 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-
+import { shallow } from 'enzyme';
import React from 'react';
-
import { SavedObjectSaveModal } from './saved_object_save_modal';
-import { shallow } from 'enzyme';
+
import { mountWithIntl } from 'test_utils/enzyme_helpers';
describe('SavedObjectSaveModal', () => {
it('should render matching snapshot', () => {
const wrapper = shallow(
{}}
- onClose={() => {}}
+ onSave={() => void 0}
+ onClose={() => void 0}
title={'Saved Object title'}
showCopyOnSave={false}
objectType="visualization"
@@ -40,14 +39,16 @@ describe('SavedObjectSaveModal', () => {
it('allows specifying custom save button label', () => {
const wrapper = mountWithIntl(
{}}
- onClose={() => {}}
+ onSave={() => void 0}
+ onClose={() => void 0}
title={'Saved Object title'}
showCopyOnSave={false}
objectType="visualization"
confirmButtonLabel="Save and done"
/>
);
- expect(wrapper.find('button[data-test-subj="confirmSaveSavedObjectButton"]').text()).toBe('Save and done');
+ expect(wrapper.find('button[data-test-subj="confirmSaveSavedObjectButton"]').text()).toBe(
+ 'Save and done'
+ );
});
});
diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.js b/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.tsx
similarity index 66%
rename from src/legacy/ui/public/saved_objects/components/saved_object_save_modal.js
rename to src/legacy/ui/public/saved_objects/components/saved_object_save_modal.tsx
index 574c09cd2a84d9..6e9b3f8337b843 100644
--- a/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.js
+++ b/src/legacy/ui/public/saved_objects/components/saved_object_save_modal.tsx
@@ -16,14 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { FormattedMessage } from '@kbn/i18n/react';
-
import {
EuiButton,
+ EuiCallOut,
EuiFieldText,
+ EuiForm,
+ EuiFormRow,
EuiModal,
EuiModalBody,
EuiModalFooter,
@@ -31,33 +29,127 @@ import {
EuiModalHeaderTitle,
EuiOverlayMask,
EuiSpacer,
- EuiCallOut,
- EuiForm,
- EuiFormRow,
EuiSwitch,
} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React, { Fragment } from 'react';
-export class SavedObjectSaveModal extends React.Component {
- constructor(props) {
- super(props);
+interface OnSaveProps {
+ newTitle: string;
+ newCopyOnSave: boolean;
+ isTitleDuplicateConfirmed: boolean;
+ onTitleDuplicate: () => void;
+}
- this.state = {
- title: props.title,
- copyOnSave: false,
- isTitleDuplicateConfirmed: false,
- hasTitleDuplicate: false,
- isLoading: false,
- };
- }
- componentDidMount() {
- this._isMounted = true;
- }
+interface Props {
+ onSave: (props: OnSaveProps) => void;
+ onClose: () => void;
+ title: string;
+ showCopyOnSave: boolean;
+ objectType: string;
+ confirmButtonLabel?: React.ReactNode;
+ options?: React.ReactNode;
+}
+
+interface State {
+ title: string;
+ copyOnSave: boolean;
+ isTitleDuplicateConfirmed: boolean;
+ hasTitleDuplicate: boolean;
+ isLoading: boolean;
+}
+
+export class SavedObjectSaveModal extends React.Component {
+ public readonly state = {
+ title: this.props.title,
+ copyOnSave: false,
+ isTitleDuplicateConfirmed: false,
+ hasTitleDuplicate: false,
+ isLoading: false,
+ };
+
+ public render() {
+ const { isTitleDuplicateConfirmed, hasTitleDuplicate, title, isLoading } = this.state;
+
+ return (
+
+
+
+
+
+ );
}
- onTitleDuplicate = () => {
+ private onTitleDuplicate = () => {
this.setState({
isLoading: false,
isTitleDuplicateConfirmed: true,
@@ -65,7 +157,7 @@ export class SavedObjectSaveModal extends React.Component {
});
};
- saveSavedObject = async () => {
+ private saveSavedObject = async () => {
if (this.state.isLoading) {
// ignore extra clicks
return;
@@ -83,7 +175,7 @@ export class SavedObjectSaveModal extends React.Component {
});
};
- onTitleChange = (event) => {
+ private onTitleChange = (event: React.ChangeEvent) => {
this.setState({
title: event.target.value,
isTitleDuplicateConfirmed: false,
@@ -91,18 +183,18 @@ export class SavedObjectSaveModal extends React.Component {
});
};
- onCopyOnSaveChange = (event) => {
+ private onCopyOnSaveChange = (event: React.ChangeEvent) => {
this.setState({
copyOnSave: event.target.checked,
});
};
- onFormSubmit = (event) => {
+ private onFormSubmit = (event: React.FormEvent) => {
event.preventDefault();
this.saveSavedObject();
};
- renderDuplicateTitleCallout = () => {
+ private renderDuplicateTitleCallout = () => {
if (!this.state.hasTitleDuplicate) {
return;
}
@@ -110,11 +202,13 @@ export class SavedObjectSaveModal extends React.Component {
return (
)}
+ title={
+
+ }
color="warning"
data-test-subj="titleDupicateWarnMsg"
>
@@ -131,7 +225,7 @@ export class SavedObjectSaveModal extends React.Component {
defaultMessage="Confirm Save"
/>
- )
+ ),
}}
/>
@@ -141,18 +235,20 @@ export class SavedObjectSaveModal extends React.Component {
);
};
- renderCopyOnSave = () => {
+ private renderCopyOnSave = () => {
if (!this.props.showCopyOnSave) {
return;
}
return (
)}
+ label={
+
+ }
>
);
};
-
- render() {
- const { isTitleDuplicateConfirmed, hasTitleDuplicate, title, isLoading } = this.state;
-
- return (
-
-
-
-
-
- );
- }
}
-
-SavedObjectSaveModal.propTypes = {
- onSave: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
- title: PropTypes.string.isRequired,
- showCopyOnSave: PropTypes.bool.isRequired,
- objectType: PropTypes.string.isRequired,
- confirmButtonLabel: PropTypes.node,
- options: PropTypes.node,
-};
diff --git a/src/legacy/ui/public/vis/editors/default/_sidebar.scss b/src/legacy/ui/public/vis/editors/default/_sidebar.scss
index 2fcc0e79f4a420..db9c3337beed45 100644
--- a/src/legacy/ui/public/vis/editors/default/_sidebar.scss
+++ b/src/legacy/ui/public/vis/editors/default/_sidebar.scss
@@ -97,7 +97,7 @@
margin-top: $euiSizeS;
}
- label {
+ label:not([class^="eui"]) {
@include __legacyLabelStyles__bad;
display: block;
}
diff --git a/src/legacy/ui/public/vis/editors/default/agg_param.js b/src/legacy/ui/public/vis/editors/default/agg_param.js
index 0517ded735b74d..7ce49a801bd6a0 100644
--- a/src/legacy/ui/public/vis/editors/default/agg_param.js
+++ b/src/legacy/ui/public/vis/editors/default/agg_param.js
@@ -88,6 +88,7 @@ uiModules
// we store the new value in $scope.paramValue, which will be passed as a new value to the react component.
$scope.paramValue = value;
}, true);
+ $scope.paramValue = $scope.agg.params[$scope.aggParam.name];
}
$scope.onChange = (value) => {
diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap b/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap
index 776e62070b8e1c..ee4f3354cc9ed9 100644
--- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap
+++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap
@@ -2,7 +2,7 @@
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles input_control_vis function 1`] = `"input_control_vis visConfig='{\\"some\\":\\"nested\\",\\"data\\":{\\"here\\":true}}' "`;
-exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles markdown function 1`] = `"kibana_markdown visConfig='{\\"markdown\\":\\"## hello _markdown_\\",\\"foo\\":\\"bar\\"}' "`;
+exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles markdown function 1`] = `"markdownvis '## hello _markdown_' fontSize=12 openLinksInNewTab=true "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function with buckets 1`] = `"kibana_metric visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metrics\\":[0,1],\\"bucket\\":2}}' "`;
diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js
index 69fe781d5ef5ab..a38b567b2f5896 100644
--- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js
+++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js
@@ -87,7 +87,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
});
it('handles markdown function', () => {
- const params = { markdown: '## hello _markdown_', foo: 'bar' };
+ const params = { markdown: '## hello _markdown_', fontSize: 12, openLinksInNewTab: true, foo: 'bar' };
const actual = buildPipelineVisFunction.markdown({ params });
expect(actual).toMatchSnapshot();
});
diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts
index 332672eead67f2..ae1c410ee4fe70 100644
--- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts
+++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts
@@ -198,7 +198,11 @@ export const prepareJson = (variable: string, data: object): string => {
};
export const prepareString = (variable: string, data: string): string => {
- return `${variable}='${data.replace(/\\/g, `\\\\`).replace(/'/g, `\\'`)}' `;
+ return `${variable}='${escapeString(data)}' `;
+};
+
+export const escapeString = (data: string): string => {
+ return data.replace(/\\/g, `\\\\`).replace(/'/g, `\\'`);
};
export const buildPipelineVisFunction: BuildPipelineVisFunction = {
@@ -220,8 +224,16 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
return `timelion_vis ${expression}${interval}`;
},
markdown: visState => {
- const visConfig = prepareJson('visConfig', visState.params);
- return `kibana_markdown ${visConfig}`;
+ const { markdown, fontSize, openLinksInNewTab } = visState.params;
+ const escapedMarkdown = escapeString(markdown);
+ let expr = `markdownvis '${escapedMarkdown}' `;
+ if (fontSize) {
+ expr += ` fontSize=${fontSize} `;
+ }
+ if (openLinksInNewTab) {
+ expr += `openLinksInNewTab=${openLinksInNewTab} `;
+ }
+ return expr;
},
table: (visState, schemas) => {
const visConfig = {
diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.js b/test/api_integration/apis/saved_objects/resolve_import_errors.js
index 665a36fe8816ad..c51134fe009d72 100644
--- a/test/api_integration/apis/saved_objects/resolve_import_errors.js
+++ b/test/api_integration/apis/saved_objects/resolve_import_errors.js
@@ -113,6 +113,7 @@ export default function ({ getService }) {
type: 'visualization',
attributes: {
title: 'My favorite vis',
+ visState: '{}',
},
references: [
{
@@ -230,6 +231,7 @@ export default function ({ getService }) {
type: 'visualization',
attributes: {
title: 'My favorite vis',
+ visState: '{}',
},
references: [
{
diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js
index 73e5e7e0cecb3a..1c26b6429fb455 100644
--- a/test/functional/apps/management/_import_objects.js
+++ b/test/functional/apps/management/_import_objects.js
@@ -27,157 +27,331 @@ export default function ({ getService, getPageObjects }) {
const testSubjects = getService('testSubjects');
describe('import objects', function describeIndexTests() {
- beforeEach(async function () {
- // delete .kibana index and then wait for Kibana to re-create it
- await kibanaServer.uiSettings.replace({});
- await PageObjects.settings.navigateTo();
- await esArchiver.load('management');
- });
+ describe('.ndjson file', () => {
+ beforeEach(async function () {
+ // delete .kibana index and then wait for Kibana to re-create it
+ await kibanaServer.uiSettings.replace({});
+ await PageObjects.settings.navigateTo();
+ await esArchiver.load('management');
+ });
- afterEach(async function () {
- await esArchiver.unload('management');
- });
+ afterEach(async function () {
+ await esArchiver.unload('management');
+ });
- it('should import saved objects', async function () {
- await PageObjects.settings.clickKibanaSavedObjects();
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json'));
- await PageObjects.settings.clickImportDone();
- await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
- const objects = await PageObjects.settings.getSavedObjectsInTable();
- const isSavedObjectImported = objects.includes('Log Agents');
- expect(isSavedObjectImported).to.be(true);
- });
+ it('should import saved objects', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.ndjson'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('Log Agents');
+ expect(isSavedObjectImported).to.be(true);
+ });
- it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
- await PageObjects.settings.clickKibanaSavedObjects();
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects-conflicts.json'));
- await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*');
- await PageObjects.settings.clickConfirmChanges();
- await PageObjects.header.waitUntilLoadingHasFinished();
- await PageObjects.settings.clickImportDone();
- await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
- const objects = await PageObjects.settings.getSavedObjectsInTable();
- const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
- expect(isSavedObjectImported).to.be(true);
- });
+ it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_conflicts.ndjson'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*');
+ await PageObjects.settings.clickConfirmChanges();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
+ expect(isSavedObjectImported).to.be(true);
+ });
- it('should allow the user to override duplicate saved objects', async function () {
- await PageObjects.settings.clickKibanaSavedObjects();
+ it('should allow the user to override duplicate saved objects', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
- // This data has already been loaded by the "visualize" esArchive. We'll load it again
- // so that we can override the existing visualization.
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
+ // This data has already been loaded by the "visualize" esArchive. We'll load it again
+ // so that we can override the existing visualization.
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.ndjson'), false);
- await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
- await PageObjects.settings.clickConfirmChanges();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
+ await PageObjects.settings.clickConfirmChanges();
- // Override the visualization.
- await PageObjects.common.clickConfirmOnModal();
+ // Override the visualization.
+ await PageObjects.common.clickConfirmOnModal();
- const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
- expect(isSuccessful).to.be(true);
- });
+ const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
+ expect(isSuccessful).to.be(true);
+ });
- it('should allow the user to cancel overriding duplicate saved objects', async function () {
- await PageObjects.settings.clickKibanaSavedObjects();
+ it('should allow the user to cancel overriding duplicate saved objects', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
- // This data has already been loaded by the "visualize" esArchive. We'll load it again
- // so that we can be prompted to override the existing visualization.
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
+ // This data has already been loaded by the "visualize" esArchive. We'll load it again
+ // so that we can be prompted to override the existing visualization.
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.ndjson'), false);
- await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
- await PageObjects.settings.clickConfirmChanges();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
+ await PageObjects.settings.clickConfirmChanges();
- // *Don't* override the visualization.
- await PageObjects.common.clickCancelOnModal();
+ // *Don't* override the visualization.
+ await PageObjects.common.clickCancelOnModal();
- const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
- expect(isSuccessful).to.be(true);
- });
+ const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
+ expect(isSuccessful).to.be(true);
+ });
- it('should import saved objects linked to saved searches', async function () {
- await PageObjects.settings.clickKibanaSavedObjects();
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
- await PageObjects.settings.clickImportDone();
+ it('should import saved objects linked to saved searches', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.ndjson'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
- await PageObjects.settings.navigateTo();
- await PageObjects.settings.clickKibanaSavedObjects();
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
- await PageObjects.settings.clickImportDone();
- await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
- const objects = await PageObjects.settings.getSavedObjectsInTable();
- const isSavedObjectImported = objects.includes('saved object connected to saved search');
- expect(isSavedObjectImported).to.be(true);
- });
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object connected to saved search');
+ expect(isSavedObjectImported).to.be(true);
+ });
- it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
- await PageObjects.settings.navigateTo();
- await PageObjects.settings.clickKibanaSavedObjects();
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
- await PageObjects.settings.clickImportDone();
- await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+ it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
- const objects = await PageObjects.settings.getSavedObjectsInTable();
- const isSavedObjectImported = objects.includes('saved object connected to saved search');
- expect(isSavedObjectImported).to.be(false);
- });
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object connected to saved search');
+ expect(isSavedObjectImported).to.be(false);
+ });
- it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
- // First, import the saved search
- await PageObjects.settings.clickKibanaSavedObjects();
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
- await PageObjects.settings.clickImportDone();
-
- // Second, we need to delete the index pattern
- await PageObjects.settings.navigateTo();
- await PageObjects.settings.clickKibanaIndexPatterns();
- await PageObjects.settings.clickIndexPatternLogstash();
- await PageObjects.settings.removeIndexPattern();
-
- // Last, import a saved object connected to the saved search
- // This should NOT show the conflicts
- await PageObjects.settings.navigateTo();
- await PageObjects.settings.clickKibanaSavedObjects();
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
- await PageObjects.settings.clickImportDone();
- await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
-
- const objects = await PageObjects.settings.getSavedObjectsInTable();
- const isSavedObjectImported = objects.includes('saved object connected to saved search');
- expect(isSavedObjectImported).to.be(false);
- });
+ it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaIndexPatterns();
+ await PageObjects.settings.clickIndexPatternLogstash();
+ await PageObjects.settings.removeIndexPattern();
+
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_saved_search.ndjson'));
+ // Wait for all the saves to happen
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickConfirmChanges();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object connected to saved search');
+ expect(isSavedObjectImported).to.be(false);
+ });
- it('should import saved objects with index patterns when index patterns already exists', async () => {
- // First, import the objects
- await PageObjects.settings.clickKibanaSavedObjects();
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
- await PageObjects.settings.clickImportDone();
- // Wait for all the saves to happen
- await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
-
- const objects = await PageObjects.settings.getSavedObjectsInTable();
- const isSavedObjectImported = objects.includes('saved object imported with index pattern');
- expect(isSavedObjectImported).to.be(true);
+ it('should import saved objects with index patterns when index patterns already exists', async () => {
+ // First, import the objects
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ // Wait for all the saves to happen
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object imported with index pattern');
+ expect(isSavedObjectImported).to.be(true);
+ });
+
+ it('should import saved objects with index patterns when index patterns does not exists', async () => {
+ // First, we need to delete the index pattern
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaIndexPatterns();
+ await PageObjects.settings.clickIndexPatternLogstash();
+ await PageObjects.settings.removeIndexPattern();
+
+ // Then, import the objects
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ // Wait for all the saves to happen
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object imported with index pattern');
+ expect(isSavedObjectImported).to.be(true);
+ });
});
- it('should import saved objects with index patterns when index patterns does not exists', async () => {
- // First, we need to delete the index pattern
- await PageObjects.settings.navigateTo();
- await PageObjects.settings.clickKibanaIndexPatterns();
- await PageObjects.settings.clickIndexPatternLogstash();
- await PageObjects.settings.removeIndexPattern();
-
- // Then, import the objects
- await PageObjects.settings.clickKibanaSavedObjects();
- await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
- await PageObjects.settings.clickImportDone();
- // Wait for all the saves to happen
- await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
-
- const objects = await PageObjects.settings.getSavedObjectsInTable();
- const isSavedObjectImported = objects.includes('saved object imported with index pattern');
- expect(isSavedObjectImported).to.be(true);
+ describe('.json file', () => {
+ beforeEach(async function () {
+ // delete .kibana index and then wait for Kibana to re-create it
+ await kibanaServer.uiSettings.replace({});
+ await PageObjects.settings.navigateTo();
+ await esArchiver.load('management');
+ });
+
+ afterEach(async function () {
+ await esArchiver.unload('management');
+ });
+
+ it('should import saved objects', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('Log Agents');
+ expect(isSavedObjectImported).to.be(true);
+ });
+
+ it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects-conflicts.json'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*');
+ await PageObjects.settings.clickConfirmChanges();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
+ expect(isSavedObjectImported).to.be(true);
+ });
+
+ it('should allow the user to override duplicate saved objects', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
+
+ // This data has already been loaded by the "visualize" esArchive. We'll load it again
+ // so that we can override the existing visualization.
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
+ await PageObjects.settings.clickConfirmChanges();
+
+ // Override the visualization.
+ await PageObjects.common.clickConfirmOnModal();
+
+ const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
+ expect(isSuccessful).to.be(true);
+ });
+
+ it('should allow the user to cancel overriding duplicate saved objects', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
+
+ // This data has already been loaded by the "visualize" esArchive. We'll load it again
+ // so that we can be prompted to override the existing visualization.
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
+
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
+ await PageObjects.settings.clickConfirmChanges();
+
+ // *Don't* override the visualization.
+ await PageObjects.common.clickCancelOnModal();
+
+ const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
+ expect(isSuccessful).to.be(true);
+ });
+
+ it('should import saved objects linked to saved searches', async function () {
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object connected to saved search');
+ expect(isSavedObjectImported).to.be(true);
+ });
+
+ it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object connected to saved search');
+ expect(isSavedObjectImported).to.be(false);
+ });
+
+ it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
+ // First, import the saved search
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
+ // Wait for all the saves to happen
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+
+ // Second, we need to delete the index pattern
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaIndexPatterns();
+ await PageObjects.settings.clickIndexPatternLogstash();
+ await PageObjects.settings.removeIndexPattern();
+
+ // Last, import a saved object connected to the saved search
+ // This should NOT show the conflicts
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'));
+ // Wait for all the saves to happen
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object connected to saved search');
+ expect(isSavedObjectImported).to.be(false);
+ });
+
+ it('should import saved objects with index patterns when index patterns already exists', async () => {
+ // First, import the objects
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ // Wait for all the saves to happen
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object imported with index pattern');
+ expect(isSavedObjectImported).to.be(true);
+ });
+
+ it('should import saved objects with index patterns when index patterns does not exists', async () => {
+ // First, we need to delete the index pattern
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaIndexPatterns();
+ await PageObjects.settings.clickIndexPatternLogstash();
+ await PageObjects.settings.removeIndexPattern();
+
+ // Then, import the objects
+ await PageObjects.settings.clickKibanaSavedObjects();
+ await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'));
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.settings.clickImportDone();
+ // Wait for all the saves to happen
+ await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+
+ const objects = await PageObjects.settings.getSavedObjectsInTable();
+ const isSavedObjectImported = objects.includes('saved object imported with index pattern');
+ expect(isSavedObjectImported).to.be(true);
+ });
});
});
}
diff --git a/test/functional/apps/management/exports/_import_objects.ndjson b/test/functional/apps/management/exports/_import_objects.ndjson
new file mode 100644
index 00000000000000..3511fb44cdfb24
--- /dev/null
+++ b/test/functional/apps/management/exports/_import_objects.ndjson
@@ -0,0 +1 @@
+{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}
diff --git a/test/functional/apps/management/exports/_import_objects_conflicts.ndjson b/test/functional/apps/management/exports/_import_objects_conflicts.ndjson
new file mode 100644
index 00000000000000..88c1830ec588fa
--- /dev/null
+++ b/test/functional/apps/management/exports/_import_objects_conflicts.ndjson
@@ -0,0 +1 @@
+{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"saved object with index pattern conflict","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"saved_object_with_index_pattern_conflict","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"d1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}
diff --git a/test/functional/apps/management/exports/_import_objects_connected_to_saved_search.ndjson b/test/functional/apps/management/exports/_import_objects_connected_to_saved_search.ndjson
new file mode 100644
index 00000000000000..803da8a639bc9d
--- /dev/null
+++ b/test/functional/apps/management/exports/_import_objects_connected_to_saved_search.ndjson
@@ -0,0 +1 @@
+{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"},"savedSearchRefName":"search_0","title":"saved object connected to saved search","uiStateJSON":"{}","visState":"{\"title\":\"PHP Viz\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"},"id":"saved_object_connected_to_saved_search","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","name":"search_0","type":"search"}],"type":"visualization","version":1}
diff --git a/test/functional/apps/management/exports/_import_objects_exists.ndjson b/test/functional/apps/management/exports/_import_objects_exists.ndjson
new file mode 100644
index 00000000000000..e0261f07b5bc59
--- /dev/null
+++ b/test/functional/apps/management/exports/_import_objects_exists.ndjson
@@ -0,0 +1 @@
+{"attributes":{"description":"AreaChart","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Shared-Item Visualization AreaChart","uiStateJSON":"{}","visState":"{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"},"id":"Shared-Item-Visualization-AreaChart","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"logstash-*","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}
diff --git a/test/functional/apps/management/exports/_import_objects_saved_search.ndjson b/test/functional/apps/management/exports/_import_objects_saved_search.ndjson
new file mode 100644
index 00000000000000..4e2e350ec59fe3
--- /dev/null
+++ b/test/functional/apps/management/exports/_import_objects_saved_search.ndjson
@@ -0,0 +1 @@
+{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"php\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":["@timestamp","desc"],"title":"PHP saved search"},"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","migrationVersion":{"search":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","version":1}
diff --git a/test/functional/apps/management/exports/_import_objects_with_index_patterns.ndjson b/test/functional/apps/management/exports/_import_objects_with_index_patterns.ndjson
new file mode 100644
index 00000000000000..aa990b27519446
--- /dev/null
+++ b/test/functional/apps/management/exports/_import_objects_with_index_patterns.ndjson
@@ -0,0 +1,2 @@
+{"attributes":{"fields":"[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"expression script\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value\",\"lang\":\"expression\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false}]","timeFieldName":"@timestamp","title":"logstash-*"},"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","type":"index-pattern"}
+{"attributes":{"description":"AreaChart","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"saved object imported with index pattern","uiStateJSON":"{}","visState":"{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}"},"id":"saved_object_imported_with_index_pattern","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1}
diff --git a/test/functional/apps/management/exports/_import_objects_with_saved_search.ndjson b/test/functional/apps/management/exports/_import_objects_with_saved_search.ndjson
new file mode 100644
index 00000000000000..0e321b168d68c8
--- /dev/null
+++ b/test/functional/apps/management/exports/_import_objects_with_saved_search.ndjson
@@ -0,0 +1,2 @@
+{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"php\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":["@timestamp","desc"],"title":"PHP saved search"},"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","migrationVersion":{"search":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","version":1}
+{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"},"savedSearchRefName":"search_0","title":"saved object connected to saved search","uiStateJSON":"{}","visState":"{\"title\":\"PHP Viz\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"},"id":"saved_object_connected_to_saved_search","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"c45e6c50-ba72-11e7-a8f9-ad70f02e633d","name":"search_0","type":"search"}],"type":"visualization","version":1}
diff --git a/test/functional/apps/management/exports/mgmt_import_objects.ndjson b/test/functional/apps/management/exports/mgmt_import_objects.ndjson
new file mode 100644
index 00000000000000..d94e2904ee70ee
--- /dev/null
+++ b/test/functional/apps/management/exports/mgmt_import_objects.ndjson
@@ -0,0 +1,2 @@
+{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":["@timestamp","desc"],"title":"mysavedsearch"},"id":"6aea5700-ac94-11e8-a651-614b2788174a","migrationVersion":{"search":"7.0.0"},"references":[{"id":"4c3f3c30-ac94-11e8-a651-614b2788174a","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"search","version":1}
+{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}"},"savedSearchRefName":"search_0","title":"mysavedviz","uiStateJSON":"{}","visState":"{\"title\":\"mysavedviz\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"},"id":"8411daa0-ac94-11e8-a651-614b2788174a","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"6aea5700-ac94-11e8-a651-614b2788174a","name":"search_0","type":"search"}],"type":"visualization","version":1}
diff --git a/test/functional/services/combo_box.js b/test/functional/services/combo_box.js
index dc8943a84440ce..6f629aa0aa1f23 100644
--- a/test/functional/services/combo_box.js
+++ b/test/functional/services/combo_box.js
@@ -18,11 +18,14 @@
*/
export function ComboBoxProvider({ getService }) {
+ const config = getService('config');
const testSubjects = getService('testSubjects');
const find = getService('find');
const log = getService('log');
const retry = getService('retry');
+ const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists');
+
// wrapper around EuiComboBox interactions
class ComboBox {
@@ -38,7 +41,10 @@ export function ComboBoxProvider({ getService }) {
await this.openOptionsList(comboBoxElement);
if (value !== undefined) {
- const options = await find.allByCssSelector(`.euiFilterSelectItem[title^="${value.toString().trim()}"]`);
+ const options = await find.allByCssSelector(
+ `.euiFilterSelectItem[title^="${value.toString().trim()}"]`,
+ WAIT_FOR_EXISTS_TIME
+ );
if (options.length > 0) {
await options[0].click();
@@ -92,15 +98,15 @@ export function ComboBoxProvider({ getService }) {
async doesComboBoxHaveSelectedOptions(comboBoxSelector) {
log.debug(`comboBox.doesComboBoxHaveSelectedOptions, comboBoxSelector: ${comboBoxSelector}`);
const comboBox = await testSubjects.find(comboBoxSelector);
- const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill');
- return selectedOptions > 0;
+ const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill', WAIT_FOR_EXISTS_TIME);
+ return selectedOptions.length > 0;
}
async getComboBoxSelectedOptions(comboBoxSelector) {
log.debug(`comboBox.getComboBoxSelectedOptions, comboBoxSelector: ${comboBoxSelector}`);
return await retry.try(async () => {
const comboBox = await testSubjects.find(comboBoxSelector);
- const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill');
+ const selectedOptions = await comboBox.findAllByClassName('euiComboBoxPill', WAIT_FOR_EXISTS_TIME);
if (selectedOptions.length === 0) {
return [];
}
@@ -132,8 +138,11 @@ export function ComboBoxProvider({ getService }) {
}
async doesClearButtonExist(comboBoxElement) {
- return await find.exists(
- async () => await comboBoxElement.findByCssSelector('[data-test-subj="comboBoxClearButton"]'));
+ const found = await comboBoxElement.findAllByCssSelector(
+ '[data-test-subj="comboBoxClearButton"]',
+ WAIT_FOR_EXISTS_TIME
+ );
+ return found.length > 0;
}
async closeOptionsList(comboBoxElement) {
diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.js b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.js
index be9c88fda1aa14..d8ea6a677a40cf 100644
--- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.js
+++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.js
@@ -40,6 +40,17 @@ export class WebElementWrapper {
this._logger = log;
}
+ async _findWithCustomTimeout(findFunction, timeout) {
+ if (timeout && timeout !== this._defaultFindTimeout) {
+ await this._driver.manage().setTimeouts({ implicit: timeout });
+ }
+ const elements = await findFunction();
+ if (timeout && timeout !== this._defaultFindTimeout) {
+ await this._driver.manage().setTimeouts({ implicit: this._defaultFindTimeout });
+ }
+ return elements;
+ }
+
_wrap(otherWebElement) {
return new WebElementWrapper(otherWebElement, this._webDriver, this._defaultFindTimeout, this._fixedHeaderHeight, this._logger);
}
@@ -286,10 +297,16 @@ export class WebElementWrapper {
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement
*
* @param {string} selector
+ * @param {number} timeout
* @return {Promise}
*/
- async findAllByCssSelector(selector) {
- return this._wrapAll(await this._webElement.findElements(this._By.css(selector)));
+ async findAllByCssSelector(selector, timeout) {
+ return this._wrapAll(
+ await this._findWithCustomTimeout(
+ async () => await this._webElement.findElements(this._By.css(selector)),
+ timeout
+ )
+ );
}
/**
@@ -308,11 +325,15 @@ export class WebElementWrapper {
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement
*
* @param {string} className
+ * @param {number} timeout
* @return {Promise}
*/
- async findAllByClassName(className) {
- return await this._wrapAll(
- await this._webElement.findElements(this._By.className(className))
+ async findAllByClassName(className, timeout) {
+ return this._wrapAll(
+ await this._findWithCustomTimeout(
+ async () => await this._webElement.findElements(this._By.className(className)),
+ timeout
+ )
);
}
@@ -332,11 +353,15 @@ export class WebElementWrapper {
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement
*
* @param {string} tagName
+ * @param {number} timeout
* @return {Promise}
*/
- async findAllByTagName(tagName) {
- return await this._wrapAll(
- await this._webElement.findElements(this._By.tagName(tagName))
+ async findAllByTagName(tagName, timeout) {
+ return this._wrapAll(
+ await this._findWithCustomTimeout(
+ async () => await this._webElement.findElements(this._By.tagName(tagName)),
+ timeout
+ )
);
}
@@ -356,11 +381,15 @@ export class WebElementWrapper {
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement
*
* @param {string} selector
+ * @param {number} timeout
* @return {Promise}
*/
- async findAllByXpath(selector) {
- return await this._wrapAll(
- await this._webElement.findElements(this._By.xpath(selector))
+ async findAllByXpath(selector, timeout) {
+ return this._wrapAll(
+ await this._findWithCustomTimeout(
+ async () => await this._webElement.findElements(this._By.xpath(selector)),
+ timeout
+ )
);
}
@@ -380,11 +409,15 @@ export class WebElementWrapper {
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement
*
* @param {string} selector
+ * @param {number} timeout
* @return {Promise}
*/
- async findAllByPartialLinkText(linkText) {
- return await this._wrapAll(
- await this._webElement.findElements(this._By.partialLinkText(linkText))
+ async findAllByPartialLinkText(linkText, timeout) {
+ return this._wrapAll(
+ await this._findWithCustomTimeout(
+ async () => await this._webElement.findElements(this._By.partialLinkText(linkText)),
+ timeout
+ )
);
}
diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts
index c4cf50b02046bf..ef5ef0f081df5e 100644
--- a/test/functional/services/remote/remote.ts
+++ b/test/functional/services/remote/remote.ts
@@ -33,8 +33,10 @@ export async function RemoteProvider({ getService }: FtrProviderContext) {
}
const { driver, By, Key, until, LegacyActionSequence } = await initWebDriver(log, browserType);
+ const caps = await driver.getCapabilities();
+ const browserVersion = caps.get(browserType === 'chrome' ? 'version' : 'browserVersion');
- log.info('Remote initialized');
+ log.info(`Remote initialized: ${caps.get('browserName')} ${browserVersion}`);
lifecycle.on('beforeTests', async () => {
// hard coded default, can be overridden per suite using `browser.setWindowSize()`
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
index 094c573752deff..78a7189f5070e8 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
@@ -7,7 +7,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "9.8.0",
+ "@elastic/eui": "9.9.0",
"react": "^16.8.0",
"react-dom": "^16.8.0"
}
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
index a8b92c01025acf..ad4ab8562a2d56 100644
--- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
@@ -7,7 +7,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "9.8.0",
+ "@elastic/eui": "9.9.0",
"react": "^16.8.0"
}
}
diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json
index e2999c80392a05..40824966cf306f 100644
--- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json
@@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "9.8.0",
+ "@elastic/eui": "9.9.0",
"react": "^16.8.0"
},
"scripts": {
diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx
index 41c867b5526d5c..997c84ebbe0517 100644
--- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx
+++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx
@@ -18,7 +18,7 @@
*/
import { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
import React from 'react';
-import { openFlyout } from 'ui/flyout';
+import { getNewPlatform } from 'ui/new_platform';
import {
ContextMenuAction,
@@ -38,7 +38,7 @@ class SamplePanelAction extends ContextMenuAction {
if (!embeddable) {
return;
}
- openFlyout(
+ getNewPlatform().setup.core.overlays.openFlyout(
diff --git a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json b/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json
index 44a612a7d322a6..dd96255e2c59ca 100644
--- a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json
@@ -7,7 +7,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "9.8.0",
+ "@elastic/eui": "9.9.0",
"react": "^16.8.0",
"react-dom": "^16.8.0"
}
diff --git a/x-pack/package.json b/x-pack/package.json
index c434a1b6f80c15..5553940e11f742 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -144,7 +144,7 @@
"@babel/register": "^7.0.0",
"@babel/runtime": "^7.3.4",
"@elastic/datemath": "5.0.2",
- "@elastic/eui": "9.8.0",
+ "@elastic/eui": "9.9.0",
"@elastic/node-crypto": "0.1.2",
"@elastic/numeral": "2.3.2",
"@kbn/babel-preset": "1.0.0",
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.tsx
index 1784203e646e1c..ac25cf17cb0e8e 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.tsx
@@ -18,7 +18,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { idx } from '../../../../../common/idx';
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
-import { KibanaLink } from '../../../shared/Links/KibanaLink';
+import { APMLink } from '../../../shared/Links/APMLink';
import { legacyEncodeURIComponent } from '../../../shared/Links/url_helpers';
import { StickyProperties } from '../../../shared/StickyProperties';
@@ -48,15 +48,16 @@ function TransactionLink({
)}/${legacyEncodeURIComponent(transaction.transaction.name)}`;
return (
-
{transaction.transaction.id}
-
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
index 8101c738c18594..0423665f133b90 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
@@ -21,7 +21,7 @@ import {
truncate,
unit
} from '../../../../style/variables';
-import { KibanaLink } from '../../../shared/Links/KibanaLink';
+import { APMLink } from '../../../shared/Links/APMLink';
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
function paginateItems({
@@ -36,7 +36,7 @@ function paginateItems({
return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
}
-const GroupIdLink = styled(KibanaLink)`
+const GroupIdLink = styled(APMLink)`
font-family: ${fontFamilyCode};
`;
@@ -44,7 +44,7 @@ const MessageAndCulpritCell = styled.div`
${truncate('100%')};
`;
-const MessageLink = styled(KibanaLink)`
+const MessageLink = styled(APMLink)`
font-family: ${fontFamilyCode};
font-size: ${fontSizes.large};
${truncate('100%')};
@@ -115,7 +115,7 @@ export class ErrorGroupList extends Component {
width: px(unit * 6),
render: (groupId: string) => {
return (
-
+
{groupId.slice(0, 5) || NOT_AVAILABLE_LABEL}
);
@@ -138,7 +138,7 @@ export class ErrorGroupList extends Component {
id="error-message-tooltip"
content={message || NOT_AVAILABLE_LABEL}
>
-
+
{message || NOT_AVAILABLE_LABEL}
diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx
index ad9dd310d3f0c0..f3b6e63f624767 100644
--- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx
@@ -5,14 +5,10 @@
*/
import { Location } from 'history';
-import { last, pick } from 'lodash';
+import { last } from 'lodash';
import React from 'react';
import chrome from 'ui/chrome';
-import {
- fromQuery,
- PERSISTENT_APM_PARAMS,
- toQuery
-} from '../../shared/Links/url_helpers';
+import { getAPMHref } from '../../shared/Links/APMLink';
import { Breadcrumb, ProvideBreadcrumbs } from './ProvideBreadcrumbs';
import { routes } from './routeConfig';
@@ -23,12 +19,9 @@ interface Props {
class UpdateBreadcrumbsComponent extends React.Component {
public updateHeaderBreadcrumbs() {
- const query = toQuery(this.props.location.search);
- const persistentParams = pick(query, PERSISTENT_APM_PARAMS);
- const search = fromQuery(persistentParams);
const breadcrumbs = this.props.breadcrumbs.map(({ value, match }) => ({
text: value,
- href: `#${match.url}?${search}`
+ href: getAPMHref(match.url, this.props.location.search)
}));
const current = last(breadcrumbs) || { text: '' };
diff --git a/x-pack/plugins/apm/public/components/app/Main/__test__/__snapshots__/UpdateBreadcrumbs.test.js.snap b/x-pack/plugins/apm/public/components/app/Main/__test__/__snapshots__/UpdateBreadcrumbs.test.js.snap
index 817a78658df004..3274461c6552b3 100644
--- a/x-pack/plugins/apm/public/components/app/Main/__test__/__snapshots__/UpdateBreadcrumbs.test.js.snap
+++ b/x-pack/plugins/apm/public/components/app/Main/__test__/__snapshots__/UpdateBreadcrumbs.test.js.snap
@@ -3,11 +3,11 @@
exports[`Breadcrumbs /:serviceName 1`] = `
Array [
Object {
- "href": "#/?kuery=myKuery",
+ "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "APM",
},
Object {
- "href": "#/opbeans-node?kuery=myKuery",
+ "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "opbeans-node",
},
]
@@ -16,15 +16,15 @@ Array [
exports[`Breadcrumbs /:serviceName/errors 1`] = `
Array [
Object {
- "href": "#/?kuery=myKuery",
+ "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "APM",
},
Object {
- "href": "#/opbeans-node?kuery=myKuery",
+ "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "opbeans-node",
},
Object {
- "href": "#/opbeans-node/errors?kuery=myKuery",
+ "href": "#/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "Errors",
},
]
@@ -33,19 +33,19 @@ Array [
exports[`Breadcrumbs /:serviceName/errors/:groupId 1`] = `
Array [
Object {
- "href": "#/?kuery=myKuery",
+ "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "APM",
},
Object {
- "href": "#/opbeans-node?kuery=myKuery",
+ "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "opbeans-node",
},
Object {
- "href": "#/opbeans-node/errors?kuery=myKuery",
+ "href": "#/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "Errors",
},
Object {
- "href": "#/opbeans-node/errors/myGroupId?kuery=myKuery",
+ "href": "#/opbeans-node/errors/myGroupId?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "myGroupId",
},
]
@@ -54,15 +54,15 @@ Array [
exports[`Breadcrumbs /:serviceName/transactions 1`] = `
Array [
Object {
- "href": "#/?kuery=myKuery",
+ "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "APM",
},
Object {
- "href": "#/opbeans-node?kuery=myKuery",
+ "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "opbeans-node",
},
Object {
- "href": "#/opbeans-node/transactions?kuery=myKuery",
+ "href": "#/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "Transactions",
},
]
@@ -71,15 +71,15 @@ Array [
exports[`Breadcrumbs /:serviceName/transactions/:transactionType 1`] = `
Array [
Object {
- "href": "#/?kuery=myKuery",
+ "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "APM",
},
Object {
- "href": "#/opbeans-node?kuery=myKuery",
+ "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "opbeans-node",
},
Object {
- "href": "#/opbeans-node/transactions?kuery=myKuery",
+ "href": "#/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "Transactions",
},
]
@@ -88,19 +88,19 @@ Array [
exports[`Breadcrumbs /:serviceName/transactions/:transactionType/:transactionName 1`] = `
Array [
Object {
- "href": "#/?kuery=myKuery",
+ "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "APM",
},
Object {
- "href": "#/opbeans-node?kuery=myKuery",
+ "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "opbeans-node",
},
Object {
- "href": "#/opbeans-node/transactions?kuery=myKuery",
+ "href": "#/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "Transactions",
},
Object {
- "href": "#/opbeans-node/transactions/request/my-transaction-name?kuery=myKuery",
+ "href": "#/opbeans-node/transactions/request/my-transaction-name?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "my-transaction-name",
},
]
@@ -109,7 +109,7 @@ Array [
exports[`Breadcrumbs Homepage 1`] = `
Array [
Object {
- "href": "#/?kuery=myKuery",
+ "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
"text": "APM",
},
]
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx
index e26c2007b56753..f00227aa5d0fa5 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx
@@ -5,20 +5,18 @@
*/
import { i18n } from '@kbn/i18n';
-import { Location } from 'history';
import React, { Component } from 'react';
import { toastNotifications } from 'ui/notify';
import { startMLJob } from '../../../../../services/rest/ml';
import { getAPMIndexPattern } from '../../../../../services/rest/savedObjects';
import { IUrlParams } from '../../../../../store/urlParams';
-import { MLJobLink } from '../../../../shared/Links/MLJobLink';
+import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink';
import { MachineLearningFlyoutView } from './view';
interface Props {
isOpen: boolean;
onClose: () => void;
urlParams: IUrlParams;
- location: Location;
serviceTransactionTypes: string[];
}
@@ -116,7 +114,7 @@ export class MachineLearningFlyout extends Component {
};
public addSuccessToast = () => {
- const { location, urlParams } = this.props;
+ const { urlParams } = this.props;
const { serviceName, transactionType } = urlParams;
if (!serviceName) {
@@ -146,7 +144,6 @@ export class MachineLearningFlyout extends Component {
{i18n.translate(
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText',
@@ -167,13 +164,7 @@ export class MachineLearningFlyout extends Component {
}
public render() {
- const {
- isOpen,
- onClose,
- urlParams,
- location,
- serviceTransactionTypes
- } = this.props;
+ const { isOpen, onClose, urlParams, serviceTransactionTypes } = this.props;
const { serviceName, transactionType } = urlParams;
const {
isCreatingJob,
@@ -189,7 +180,6 @@ export class MachineLearningFlyout extends Component {
void;
onClickCreate: () => void;
onClose: () => void;
@@ -46,7 +45,6 @@ const INITIAL_DATA = { count: 0, jobs: [] };
export function MachineLearningFlyoutView({
hasIndexPattern,
isCreatingJob,
- location,
onChangeTransaction,
onClickCreate,
onClose,
@@ -111,7 +109,6 @@ export function MachineLearningFlyoutView({
{i18n.translate(
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText',
@@ -136,10 +133,7 @@ export function MachineLearningFlyoutView({
defaultMessage="No APM index pattern available. To create a job, please import the APM index pattern via the {setupInstructionLink}"
values={{
setupInstructionLink: (
-
+
{i18n.translate(
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.noPatternTitle.setupInstructionLinkText',
{
@@ -188,14 +182,14 @@ export function MachineLearningFlyoutView({
Once a job is created, you can manage it and see more details in the {mlJobsPageLink}."
values={{
mlJobsPageLink: (
-
+
{i18n.translate(
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText',
{
defaultMessage: 'Machine Learning jobs management page'
}
)}
-
+
)
}}
/>{' '}
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx
index daf706c66d1c5d..3396cb6d4a2679 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx
@@ -26,7 +26,6 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Location } from 'history';
import { memoize, padLeft, range } from 'lodash';
import moment from 'moment-timezone';
import React, { Component } from 'react';
@@ -35,7 +34,7 @@ import chrome from 'ui/chrome';
import { toastNotifications } from 'ui/notify';
import { IUrlParams } from '../../../../store/urlParams';
import { XPACK_DOCS } from '../../../../utils/documentation/xpack';
-import { UnconnectedKibanaLink } from '../../../shared/Links/KibanaLink';
+import { KibanaLink } from '../../../shared/Links/KibanaLink';
import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch';
type ScheduleKey = keyof Schedule;
@@ -59,7 +58,6 @@ const SmallInput = styled.div`
interface WatcherFlyoutProps {
urlParams: IUrlParams;
onClose: () => void;
- location: Location;
isOpen: boolean;
}
@@ -253,10 +251,8 @@ export class WatcherFlyout extends Component<
}
}
)}{' '}
-
{i18n.translate(
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText',
@@ -264,7 +260,7 @@ export class WatcherFlyout extends Component<
defaultMessage: 'View watch'
}
)}
-
+
)
});
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx
index f3a9054bb8b16a..6dcf67f1e10fb4 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx
@@ -11,7 +11,6 @@ import {
EuiPopover
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { Location } from 'history';
import { memoize } from 'lodash';
import React, { Fragment } from 'react';
import chrome from 'ui/chrome';
@@ -21,7 +20,6 @@ import { MachineLearningFlyout } from './MachineLearningFlyout';
import { WatcherFlyout } from './WatcherFlyout';
interface Props {
- location: Location;
transactionTypes: string[];
urlParams: IUrlParams;
}
@@ -164,14 +162,12 @@ export class ServiceIntegrations extends React.Component {
/>
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx
index ddf5648cff4cc1..723f8db37a7800 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx
@@ -56,10 +56,7 @@ export function NoServicesMessage({ historicalDataFound }: Props) {
defaultMessage:
'You may also have old data that needs to be migrated.'
})}{' '}
-
+
{i18n.translate(
'xpack.apm.servicesTable.UpgradeAssistantLink',
{
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap
index 4af693fe711079..a3fe8a34d19ced 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap
+++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap
@@ -7,11 +7,11 @@ exports[`ErrorGroupOverview -> List should render columns correctly 1`] = `
id="service-name-tooltip"
position="top"
>
-
opbeans-python
-
+
`;
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx
index 11df7e29a56ea9..16a7f04805fa54 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx
@@ -12,7 +12,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services';
import { fontSizes, truncate } from '../../../../style/variables';
import { asDecimal, asMillis } from '../../../../utils/formatters';
-import { KibanaLink } from '../../../shared/Links/KibanaLink';
+import { APMLink } from '../../../shared/Links/APMLink';
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
interface Props {
@@ -34,7 +34,7 @@ function formatString(value?: string | null) {
return value || NOT_AVAILABLE_LABEL;
}
-const AppLink = styled(KibanaLink)`
+const AppLink = styled(APMLink)`
font-size: ${fontSizes.large};
${truncate('100%')};
`;
@@ -51,7 +51,7 @@ export const SERVICE_COLUMNS: Array<
sortable: true,
render: (serviceName: string) => (
-
+
{formatString(serviceName)}
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap
index 22b140abd3fa02..02dbe22aeb19da 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap
@@ -16,12 +16,11 @@ exports[`NoServicesMessage should show a "no services installed" message, a link
You may also have old data that needs to be migrated.
-
Learn more by visiting the Kibana Upgrade Assistant
-
+
.
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap
index b7f431249daf89..3e10ef06587e37 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap
@@ -79,7 +79,7 @@ NodeList [
Learn more by visiting the Kibana Upgrade Assistant
@@ -95,7 +95,7 @@ NodeList [
/>
= ({
);
return (
- _.service.name)}/errors`}
+ _.service.name)}/errors`}
query={{
kuery: legacyEncodeURIComponent(
`trace.id : "${transaction.trace.id}" and transaction.id : "${
@@ -80,6 +79,6 @@ export const ErrorCountBadge: React.SFC = ({
) : (
{errorCountBadge}
)}
-
+
);
};
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx
index 8ae96285f520eb..4a808be9b68dee 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx
@@ -11,7 +11,7 @@ import {
TRANSACTION_NAME
} from '../../../../../../../common/elasticsearch_fieldnames';
import { Transaction } from '../../../../../../../typings/es_schemas/ui/Transaction';
-import { KibanaLink } from '../../../../../shared/Links/KibanaLink';
+import { APMLink } from '../../../../../shared/Links/APMLink';
import { TransactionLink } from '../../../../../shared/Links/TransactionLink';
import { StickyProperties } from '../../../../../shared/StickyProperties';
@@ -31,9 +31,9 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
}),
fieldName: SERVICE_NAME,
val: (
-
+
{transaction.service.name}
-
+
),
width: '50%'
},
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx
index 36e63567667a6f..57104c021f4039 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx
@@ -103,10 +103,7 @@ export function TransactionFlyout({
-
+
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx
index 56159b503bf2c0..3762a79aaee4bd 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx
@@ -124,10 +124,7 @@ export const Transaction: React.SFC = ({
-
+
-
+
{transactionName || NOT_AVAILABLE_LABEL}
diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/view.js b/x-pack/plugins/apm/public/components/shared/KueryBar/view.js
index f5390b7384d832..796c1907701548 100644
--- a/x-pack/plugins/apm/public/components/shared/KueryBar/view.js
+++ b/x-pack/plugins/apm/public/components/shared/KueryBar/view.js
@@ -129,10 +129,7 @@ class KueryBarView extends Component {
values={{
apmIndexPatternTitle: `"${apmIndexPatternTitle}"`,
setupInstructionsLink: (
-
+
{i18n.translate(
'xpack.apm.kueryBar.setupInstructionsLinkLabel',
{ defaultMessage: 'Setup Instructions' }
diff --git a/x-pack/plugins/apm/public/components/shared/Links/APMLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/APMLink.test.tsx
new file mode 100644
index 00000000000000..610f0d5302d941
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/APMLink.test.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Location } from 'history';
+import React from 'react';
+import { getRenderedHref } from '../../../utils/testHelpers';
+import { APMLink } from './APMLink';
+
+test('APMLink should produce the correct URL', async () => {
+ const href = await getRenderedHref(
+ () => ,
+ {
+ search: '?rangeFrom=now-5h&rangeTo=now-2h'
+ } as Location
+ );
+
+ expect(href).toMatchInlineSnapshot(
+ `"#/some/path?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0&transactionId=blah"`
+ );
+});
+
+test('APMLink should retain current kuery value if it exists', async () => {
+ const href = await getRenderedHref(
+ () => ,
+ {
+ search: '?kuery=host.hostname~20~3A~20~22fakehostname~22'
+ } as Location
+ );
+
+ expect(href).toMatchInlineSnapshot(
+ `"#/some/path?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=host.hostname~20~3A~20~22fakehostname~22&transactionId=blah"`
+ );
+});
+
+test('APMLink should overwrite current kuery value if new kuery value is provided', async () => {
+ const href = await getRenderedHref(
+ () => (
+
+ ),
+ {
+ search: '?kuery=host.hostname~20~3A~20~22fakehostname~22'
+ } as Location
+ );
+
+ expect(href).toMatchInlineSnapshot(
+ `"#/some/path?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=host.os~20~3A~20~22linux~22"`
+ );
+});
diff --git a/x-pack/plugins/apm/public/components/shared/Links/APMLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/APMLink.tsx
new file mode 100644
index 00000000000000..d9edc4eaf73c4c
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/APMLink.tsx
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
+import React from 'react';
+import url from 'url';
+import { pick } from 'lodash';
+import { useLocation } from '../../../hooks/useLocation';
+import { APMQueryParams, toQuery, fromQuery } from './url_helpers';
+import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
+
+interface Props extends EuiLinkAnchorProps {
+ path?: string;
+ query?: APMQueryParams;
+ children?: React.ReactNode;
+}
+
+export const PERSISTENT_APM_PARAMS = [
+ 'kuery',
+ 'rangeFrom',
+ 'rangeTo',
+ 'refreshPaused',
+ 'refreshInterval'
+];
+
+export function getAPMHref(
+ path: string,
+ currentSearch: string, // TODO: Replace with passing in URL PARAMS here
+ query: APMQueryParams = {}
+) {
+ const currentQuery = toQuery(currentSearch);
+ const nextQuery = {
+ ...TIMEPICKER_DEFAULTS,
+ ...pick(currentQuery, PERSISTENT_APM_PARAMS),
+ ...query
+ };
+ const nextSearch = fromQuery(nextQuery);
+
+ return url.format({
+ pathname: '',
+ hash: `${path}?${nextSearch}`
+ });
+}
+
+export function APMLink({ path = '', query, ...rest }: Props) {
+ const { search } = useLocation();
+ const href = getAPMHref(path, search, query);
+ return ;
+}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx
index 3566a996c97b71..d5633611f09e84 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx
@@ -4,27 +4,54 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { EuiLink } from '@elastic/eui';
import React from 'react';
-import { KibanaRisonLink } from '../KibanaRisonLink';
-import { RisonAPMQueryParams } from '../rison_helpers';
-import { QueryWithIndexPattern } from './QueryWithIndexPattern';
+import chrome from 'ui/chrome';
+import url from 'url';
+import rison, { RisonValue } from 'rison-node';
+import { useAPMIndexPattern } from '../../../../hooks/useAPMIndexPattern';
+import { useLocation } from '../../../../hooks/useLocation';
+import { getTimepickerRisonData } from '../rison_helpers';
interface Props {
- query: RisonAPMQueryParams;
+ query: {
+ _a?: {
+ index?: string;
+ interval?: string;
+ query?: {
+ language: string;
+ query: string;
+ };
+ sort?: {
+ [key: string]: string;
+ };
+ };
+ };
children: React.ReactNode;
}
-export function DiscoverLink({ query, ...rest }: Props) {
- return (
-
- {queryWithIndexPattern => (
-
- )}
-
- );
+export function DiscoverLink({ query = {}, ...rest }: Props) {
+ const apmIndexPattern = useAPMIndexPattern();
+ const location = useLocation();
+
+ if (!apmIndexPattern.id) {
+ return null;
+ }
+
+ const risonQuery = {
+ _g: getTimepickerRisonData(location.search),
+ _a: {
+ ...query._a,
+ index: apmIndexPattern.id
+ }
+ };
+
+ const href = url.format({
+ pathname: chrome.addBasePath('/app/kibana'),
+ hash: `/discover?_g=${rison.encode(risonQuery._g)}&_a=${rison.encode(
+ risonQuery._a as RisonValue
+ )}`
+ });
+
+ return ;
}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/QueryWithIndexPattern.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/QueryWithIndexPattern.tsx
deleted file mode 100644
index fddb2ff28dd8bb..00000000000000
--- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/QueryWithIndexPattern.tsx
+++ /dev/null
@@ -1,55 +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, { ReactElement } from 'react';
-import {
- getAPMIndexPattern,
- ISavedObject
-} from '../../../../services/rest/savedObjects';
-import { RisonAPMQueryParams } from '../rison_helpers';
-
-export function getQueryWithIndexPattern(
- query: RisonAPMQueryParams,
- indexPattern?: ISavedObject
-) {
- if ((query._a && query._a.index) || !indexPattern) {
- return query;
- }
-
- const id = indexPattern && indexPattern.id;
- return {
- ...query,
- _a: {
- ...query._a,
- index: id
- }
- };
-}
-
-interface Props {
- query: RisonAPMQueryParams;
- children: (query: RisonAPMQueryParams) => ReactElement;
-}
-
-interface State {
- indexPattern?: ISavedObject;
-}
-
-export class QueryWithIndexPattern extends React.Component {
- constructor(props: Props) {
- super(props);
- getAPMIndexPattern().then(indexPattern => {
- this.setState({ indexPattern });
- });
- this.state = {};
- }
- public render() {
- const { children, query } = this.props;
- const { indexPattern } = this.state;
- const renderWithQuery = children;
- return renderWithQuery(getQueryWithIndexPattern(query, indexPattern));
- }
-}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx
index 5d25ce2d9c08f7..030bb0c1aad61e 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx
@@ -21,6 +21,14 @@ jest
Promise.resolve({ id: 'apm-index-pattern-id' } as savedObjects.ISavedObject)
);
+beforeAll(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => null);
+});
+
+afterAll(() => {
+ jest.restoreAllMocks();
+});
+
test('DiscoverTransactionLink should produce the correct URL', async () => {
const transaction = {
transaction: {
@@ -30,11 +38,12 @@ test('DiscoverTransactionLink should produce the correct URL', async () => {
id: '8b60bd32ecc6e1506735a8b6cfcf175c'
}
} as Transaction;
+
const href = await getRenderedHref(
() => ,
{
- location: { search: '?rangeFrom=now/w&rangeTo=now' } as Location
- }
+ search: '?rangeFrom=now/w&rangeTo=now'
+ } as Location
);
expect(href).toEqual(
@@ -48,9 +57,10 @@ test('DiscoverSpanLink should produce the correct URL', async () => {
id: 'test-span-id'
}
} as Span;
+
const href = await getRenderedHref(() => , {
- location: { search: '?rangeFrom=now/w&rangeTo=now' } as Location
- });
+ search: '?rangeFrom=now/w&rangeTo=now'
+ } as Location);
expect(href).toEqual(
`/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'span.id:"test-span-id"'))`
@@ -69,8 +79,8 @@ test('DiscoverErrorLink should produce the correct URL', async () => {
const href = await getRenderedHref(
() => ,
{
- location: { search: '?rangeFrom=now/w&rangeTo=now' } as Location
- }
+ search: '?rangeFrom=now/w&rangeTo=now'
+ } as Location
);
expect(href).toEqual(
@@ -87,11 +97,12 @@ test('DiscoverErrorLink should include optional kuery string in URL', async () =
grouping_key: 'grouping-key'
}
} as APMError;
+
const href = await getRenderedHref(
() => ,
{
- location: { search: '?rangeFrom=now/w&rangeTo=now' } as Location
- }
+ search: '?rangeFrom=now/w&rangeTo=now'
+ } as Location
);
expect(href).toEqual(
diff --git a/x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx
new file mode 100644
index 00000000000000..832cb13f3ba276
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Location } from 'history';
+import React from 'react';
+import { getRenderedHref } from '../../../utils/testHelpers';
+import { InfraLink } from './InfraLink';
+import chrome from 'ui/chrome';
+
+jest
+ .spyOn(chrome, 'addBasePath')
+ .mockImplementation(path => `/basepath${path}`);
+
+test('InfraLink produces the correct URL', async () => {
+ const href = await getRenderedHref(
+ () => ,
+ {
+ search: '?rangeFrom=now-5h&rangeTo=now-2h'
+ } as Location
+ );
+
+ expect(href).toMatchInlineSnapshot(
+ `"/basepath/app/infra#/some/path?time=1554687198"`
+ );
+});
diff --git a/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx
new file mode 100644
index 00000000000000..6bc2f0c355a232
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx
@@ -0,0 +1,33 @@
+/*
+ * 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 { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
+import { compact } from 'lodash';
+import React from 'react';
+import chrome from 'ui/chrome';
+import url from 'url';
+import { fromQuery } from './url_helpers';
+
+interface InfraQueryParams {
+ time?: number;
+ from?: number;
+ to?: number;
+}
+
+interface Props extends EuiLinkAnchorProps {
+ path?: string;
+ query: InfraQueryParams;
+ children?: React.ReactNode;
+}
+
+export function InfraLink({ path, query = {}, ...rest }: Props) {
+ const nextSearch = fromQuery(query);
+ const href = url.format({
+ pathname: chrome.addBasePath('/app/infra'),
+ hash: compact([path, nextSearch]).join('?')
+ });
+ return ;
+}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx
index 7fa1c2a5f554a0..e1cf2f5f4d5628 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx
@@ -4,67 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { shallow } from 'enzyme';
import { Location } from 'history';
import React from 'react';
-import { UnconnectedKibanaLink } from './KibanaLink';
+import { getRenderedHref } from '../../../utils/testHelpers';
+import { KibanaLink } from './KibanaLink';
+import chrome from 'ui/chrome';
-const getLinkWrapper = ({
- search = '',
- pathname = '/app/kibana',
- hash = '/something',
- children = 'Some link text',
- query = {}
-} = {}) =>
- shallow(
-
- );
+jest
+ .spyOn(chrome, 'addBasePath')
+ .mockImplementation(path => `/basepath${path}`);
-describe('UnconnectedKibanaLink', () => {
- it('should render correct markup', () => {
- expect(getLinkWrapper()).toMatchSnapshot();
- });
+test('KibanaLink produces the correct URL', async () => {
+ const href = await getRenderedHref(() => , {
+ search: '?rangeFrom=now-5h&rangeTo=now-2h'
+ } as Location);
- it('should include valid query params', () => {
- const wrapper = getLinkWrapper({ query: { transactionId: 'test-id' } });
- expect(wrapper.find('EuiLink').props().href).toEqual(
- '/app/kibana#/something?transactionId=test-id'
- );
- });
-
- it('should include existing APM params for APM links', () => {
- const wrapper = getLinkWrapper({
- pathname: '/app/apm',
- search: '?rangeFrom=now-5w&rangeTo=now-2w'
- });
- expect(wrapper.find('EuiLink').props().href).toEqual(
- `/app/apm#/something?rangeFrom=now-5w&rangeTo=now-2w&refreshPaused=true&refreshInterval=0`
- );
- });
-
- it('should include APM params when the pathname is an empty string', () => {
- const wrapper = getLinkWrapper({
- pathname: '',
- search: '?rangeFrom=now-5w&rangeTo=now-2w'
- });
- expect(wrapper.find('EuiLink').props().href).toEqual(
- `#/something?rangeFrom=now-5w&rangeTo=now-2w&refreshPaused=true&refreshInterval=0`
- );
- });
-
- it('should NOT include APM params for non-APM links', () => {
- const wrapper = getLinkWrapper({
- pathname: '/app/something-else',
- search: '?rangeFrom=now-5w&rangeTo=now-2w'
- });
- expect(wrapper.find('EuiLink').props().href).toEqual(
- `/app/something-else#/something?`
- );
- });
+ expect(href).toMatchInlineSnapshot(`"/basepath/app/kibana#/some/path"`);
});
diff --git a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx
index eb2778d3119ca7..cc558a35bf609f 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx
@@ -4,47 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiLink } from '@elastic/eui';
-import { Location } from 'history';
+import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
import React from 'react';
-import { connect } from 'react-redux';
-import { StringMap } from '../../../../typings/common';
-import { getKibanaHref, KibanaHrefArgs } from './url_helpers';
+import chrome from 'ui/chrome';
+import url from 'url';
-interface Props extends KibanaHrefArgs {
- disabled?: boolean;
- to?: StringMap;
- className?: string;
- [prop: string]: any;
+interface Props extends EuiLinkAnchorProps {
+ path?: string;
+ children?: React.ReactNode;
}
-/**
- * NOTE: Use this component directly if you have to use a link that is
- * going to be rendered outside of React, e.g. in the Kibana global toast loader.
- *
- * You must remember to pass in location in that case.
- */
-const UnconnectedKibanaLink: React.FunctionComponent = ({
- location,
- pathname,
- hash,
- query,
- ...props
-}) => {
- const href = getKibanaHref({
- location,
- pathname,
- hash,
- query
+export function KibanaLink({ path, ...rest }: Props) {
+ const href = url.format({
+ pathname: chrome.addBasePath('/app/kibana'),
+ hash: path
});
- return ;
-};
-
-const withLocation = connect(
- ({ location }: { location: Location }) => ({ location }),
- {}
-);
-
-const KibanaLink = withLocation(UnconnectedKibanaLink);
-
-export { UnconnectedKibanaLink, KibanaLink };
+ return ;
+}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/KibanaRisonLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaRisonLink.test.tsx
deleted file mode 100644
index ee0885780d44ef..00000000000000
--- a/x-pack/plugins/apm/public/components/shared/Links/KibanaRisonLink.test.tsx
+++ /dev/null
@@ -1,90 +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 { shallow } from 'enzyme';
-import { Location } from 'history';
-import React from 'react';
-import { UnconnectedKibanaRisonLink } from './KibanaRisonLink';
-
-const getLinkWrapper = ({
- search = '',
- pathname = '/app/kibana',
- hash = '/discover',
- children = 'Some discover link text',
- query = {}
-} = {}) =>
- shallow(
-
- );
-
-const DEFAULT_RISON_G = `(refreshInterval:(pause:true,value:'0'),time:(from:now-24h,to:now))`;
-
-describe('UnconnectedKibanaLink', () => {
- it('should render correct markup', () => {
- expect(getLinkWrapper()).toMatchSnapshot();
- });
-
- it('should include default time picker values, rison-encoded', () => {
- const wrapper = getLinkWrapper();
- expect(wrapper.find('EuiLink').props().href).toEqual(
- expect.stringContaining(DEFAULT_RISON_G)
- );
- });
-
- it('should ignore new query params except for _g and _a', () => {
- const wrapper = getLinkWrapper({ query: { transactionId: 'test-id' } });
- expect(wrapper.find('EuiLink').props().href).not.toEqual(
- expect.stringContaining('transactionId')
- );
- });
-
- it('should rison-encode and merge in custom _g value', () => {
- const wrapper = getLinkWrapper({
- query: {
- _g: {
- something: {
- nested: 'custom g value'
- }
- }
- }
- });
-
- expect(wrapper.find('EuiLink').props().href).toEqual(
- expect.stringContaining(`something:(nested:'custom g value')`)
- );
- });
-
- it('should rison-encode custom _a value', () => {
- const wrapper = getLinkWrapper({
- query: {
- _a: {
- something: {
- nested: 'custom a value'
- }
- }
- }
- });
- expect(wrapper.find('EuiLink').props().href).toEqual(
- expect.stringContaining(`_a=(something:(nested:'custom a value'))`)
- );
- });
-
- it('should convert, url-encode, and rison-encode existing time picker values', () => {
- const wrapper = getLinkWrapper({
- search:
- '?rangeFrom=now/w&rangeTo=now&refreshPaused=false&refreshInterval=30000'
- });
- expect(wrapper.find('EuiLink').props().href).toEqual(
- "/app/kibana#/discover?_g=(refreshInterval:(pause:false,value:'30000'),time:(from:now%2Fw,to:now))"
- );
- });
-});
diff --git a/x-pack/plugins/apm/public/components/shared/Links/KibanaRisonLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaRisonLink.tsx
deleted file mode 100644
index 0cd6f9f64d9728..00000000000000
--- a/x-pack/plugins/apm/public/components/shared/Links/KibanaRisonLink.tsx
+++ /dev/null
@@ -1,49 +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 { EuiLink } from '@elastic/eui';
-import { Location } from 'history';
-import React from 'react';
-import { connect } from 'react-redux';
-import { StringMap } from '../../../../typings/common';
-import { getRisonHref, RisonHrefArgs } from './rison_helpers';
-
-interface Props extends RisonHrefArgs {
- disabled?: boolean;
- to?: StringMap;
- className?: string;
-}
-
-/**
- * NOTE: Use this component directly if you have to use a link that is
- * going to be rendered outside of React, e.g. in the Kibana global toast loader.
- *
- * You must remember to pass in location in that case.
- */
-const UnconnectedKibanaRisonLink: React.FunctionComponent = ({
- location,
- pathname,
- hash,
- query,
- ...props
-}) => {
- const href = getRisonHref({
- location,
- pathname,
- hash,
- query
- });
- return ;
-};
-
-const withLocation = connect(
- ({ location }: { location: Location }) => ({ location }),
- {}
-);
-
-const KibanaRisonLink = withLocation(UnconnectedKibanaRisonLink);
-
-export { UnconnectedKibanaRisonLink, KibanaRisonLink };
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
similarity index 50%
rename from x-pack/plugins/apm/public/components/shared/Links/MLJobLink.test.tsx
rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
index 3b37e46ed0540b..0f84b3614cb4f2 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/MLJobLink.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
@@ -4,35 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { shallow } from 'enzyme';
import { Location } from 'history';
import React from 'react';
-import { getRenderedHref } from '../../../utils/testHelpers';
+import { getRenderedHref } from '../../../../utils/testHelpers';
import { MLJobLink } from './MLJobLink';
describe('MLJobLink', () => {
- it('should render component', () => {
- const location = { search: '' } as Location;
- const wrapper = shallow(
-
- );
-
- expect(wrapper).toMatchSnapshot();
- });
-
it('should produce the correct URL', async () => {
- const location = { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location;
- const href = await getRenderedHref(() => (
-
- ));
+ const href = await getRenderedHref(
+ () => (
+
+ ),
+ { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location
+ );
expect(href).toEqual(
`/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))`
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx
similarity index 53%
rename from x-pack/plugins/apm/public/components/shared/Links/MLJobLink.tsx
rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx
index 8d6d507d0713e0..1cf10d13e57bba 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/MLJobLink.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx
@@ -4,36 +4,26 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiLink } from '@elastic/eui';
-import { Location } from 'history';
import React from 'react';
-import { getMlJobId } from '../../../../common/ml_job_constants';
-import { getRisonHref } from './rison_helpers';
+import { getMlJobId } from '../../../../../common/ml_job_constants';
+import { MLLink } from './MLLink';
interface Props {
serviceName: string;
transactionType?: string;
- location: Location;
}
export const MLJobLink: React.SFC = ({
serviceName,
transactionType,
- location,
children
}) => {
- const pathname = '/app/ml';
- const hash = '/timeseriesexplorer';
const jobId = getMlJobId(serviceName, transactionType);
const query = {
- _g: { ml: { jobIds: [jobId] } }
+ ml: { jobIds: [jobId] }
};
- const href = getRisonHref({
- location,
- pathname,
- hash,
- query
- });
- return ;
+ return (
+
+ );
};
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx
new file mode 100644
index 00000000000000..edc6e645913efe
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 { Location } from 'history';
+import React from 'react';
+import { getRenderedHref } from '../../../../utils/testHelpers';
+import { MLLink } from './MLLink';
+import chrome from 'ui/chrome';
+import * as savedObjects from '../../../../services/rest/savedObjects';
+
+jest
+ .spyOn(chrome, 'addBasePath')
+ .mockImplementation(path => `/basepath${path}`);
+
+jest
+ .spyOn(savedObjects, 'getAPMIndexPattern')
+ .mockReturnValue(
+ Promise.resolve({ id: 'apm-index-pattern-id' } as savedObjects.ISavedObject)
+ );
+
+beforeAll(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => null);
+});
+
+afterAll(() => {
+ jest.restoreAllMocks();
+});
+
+test('MLLink produces the correct URL', async () => {
+ const href = await getRenderedHref(
+ () => (
+
+ ),
+ {
+ search: '?rangeFrom=now-5h&rangeTo=now-2h'
+ } as Location
+ );
+
+ expect(href).toMatchInlineSnapshot(
+ `"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:true,value:'0'),time:(from:now-5h,to:now-2h))"`
+ );
+});
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx
new file mode 100644
index 00000000000000..949c64a2171dc6
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiLink } from '@elastic/eui';
+import React from 'react';
+import chrome from 'ui/chrome';
+import url from 'url';
+import rison, { RisonValue } from 'rison-node';
+import { useLocation } from '../../../../hooks/useLocation';
+import { getTimepickerRisonData, TimepickerRisonData } from '../rison_helpers';
+
+interface MlRisonData {
+ ml?: {
+ jobIds: string[];
+ };
+}
+
+interface Props {
+ query?: MlRisonData;
+ path?: string;
+ children?: React.ReactNode;
+}
+
+export function MLLink({ children, path = '', query = {} }: Props) {
+ const location = useLocation();
+
+ const risonQuery: MlRisonData & TimepickerRisonData = getTimepickerRisonData(
+ location.search
+ );
+
+ if (query.ml) {
+ risonQuery.ml = query.ml;
+ }
+
+ const href = url.format({
+ pathname: chrome.addBasePath('/app/ml'),
+ hash: `${path}?_g=${rison.encode(risonQuery as RisonValue)}`
+ });
+
+ return ;
+}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx
index a4f4c7c8c12427..f4ddbf82dea101 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx
@@ -15,7 +15,7 @@ export function SetupInstructionsLink({
buttonFill?: boolean;
}) {
return (
-
+
{i18n.translate('xpack.apm.setupInstructionsButtonLabel', {
defaultMessage: 'Setup Instructions'
diff --git a/x-pack/plugins/apm/public/components/shared/Links/TransactionLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/TransactionLink.tsx
index b32f1d55f8465f..9866c7db4760e8 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/TransactionLink.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/TransactionLink.tsx
@@ -6,33 +6,13 @@
import React from 'react';
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
-import { KibanaLink } from './KibanaLink';
+import { APMLink } from './APMLink';
import { legacyEncodeURIComponent } from './url_helpers';
interface TransactionLinkProps {
transaction?: Transaction;
}
-/**
- * Return the path and query used to build a trace link
- */
-export function getLinkProps(transaction: Transaction) {
- const serviceName = transaction.service.name;
- const transactionType = transaction.transaction.type;
- const traceId = transaction.trace.id;
- const transactionId = transaction.transaction.id;
- const name = transaction.transaction.name;
- const encodedName = legacyEncodeURIComponent(name);
-
- return {
- hash: `/${serviceName}/transactions/${transactionType}/${encodedName}`,
- query: {
- traceId,
- transactionId
- }
- };
-}
-
export const TransactionLink: React.SFC = ({
transaction,
children
@@ -41,12 +21,19 @@ export const TransactionLink: React.SFC = ({
return null;
}
- const linkProps = getLinkProps(transaction);
-
- if (!linkProps) {
- // TODO: Should this case return unlinked children, null, or something else?
- return {children} ;
- }
+ const serviceName = transaction.service.name;
+ const transactionType = transaction.transaction.type;
+ const traceId = transaction.trace.id;
+ const transactionId = transaction.transaction.id;
+ const name = transaction.transaction.name;
+ const encodedName = legacyEncodeURIComponent(name);
- return {children} ;
+ return (
+
+ {children}
+
+ );
};
diff --git a/x-pack/plugins/apm/public/components/shared/Links/__snapshots__/KibanaLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/__snapshots__/KibanaLink.test.tsx.snap
deleted file mode 100644
index 9aaa0d7fe629c1..00000000000000
--- a/x-pack/plugins/apm/public/components/shared/Links/__snapshots__/KibanaLink.test.tsx.snap
+++ /dev/null
@@ -1,11 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`UnconnectedKibanaLink should render correct markup 1`] = `
-
- Some link text
-
-`;
diff --git a/x-pack/plugins/apm/public/components/shared/Links/__snapshots__/KibanaRisonLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/__snapshots__/KibanaRisonLink.test.tsx.snap
deleted file mode 100644
index e46f25aa1d095d..00000000000000
--- a/x-pack/plugins/apm/public/components/shared/Links/__snapshots__/KibanaRisonLink.test.tsx.snap
+++ /dev/null
@@ -1,11 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`UnconnectedKibanaLink should render correct markup 1`] = `
-
- Some discover link text
-
-`;
diff --git a/x-pack/plugins/apm/public/components/shared/Links/__snapshots__/MLJobLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/__snapshots__/MLJobLink.test.tsx.snap
deleted file mode 100644
index 203580880d145c..00000000000000
--- a/x-pack/plugins/apm/public/components/shared/Links/__snapshots__/MLJobLink.test.tsx.snap
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MLJobLink should render component 1`] = `
-
-`;
diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts
index 935629256493bf..8d045a54b70b08 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts
+++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts
@@ -4,88 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { pick, set } from 'lodash';
-import qs from 'querystring';
-import rison from 'rison-node';
-import chrome from 'ui/chrome';
-import url from 'url';
-import { StringMap } from '../../../../typings/common';
+import { Location } from 'history';
import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
-import {
- APMQueryParams,
- KibanaHrefArgs,
- PERSISTENT_APM_PARAMS,
- toQuery
-} from './url_helpers';
+import { toQuery } from './url_helpers';
-interface RisonEncoded {
- _g?: string;
- _a?: string;
-}
-
-export interface RisonDecoded {
- _g?: StringMap;
- _a?: StringMap;
-}
-
-export type RisonAPMQueryParams = APMQueryParams & RisonDecoded;
-export type RisonHrefArgs = KibanaHrefArgs;
-
-function createG(query: RisonAPMQueryParams) {
- const { _g: nextG = {} } = query;
- const g: RisonDecoded['_g'] = { ...nextG };
-
- if (typeof query.rangeFrom !== 'undefined') {
- set(g, 'time.from', encodeURIComponent(query.rangeFrom));
- }
- if (typeof query.rangeTo !== 'undefined') {
- set(g, 'time.to', encodeURIComponent(query.rangeTo));
- }
-
- if (typeof query.refreshPaused !== 'undefined') {
- set(g, 'refreshInterval.pause', String(query.refreshPaused));
- }
- if (typeof query.refreshInterval !== 'undefined') {
- set(g, 'refreshInterval.value', String(query.refreshInterval));
- }
-
- return g;
+export interface TimepickerRisonData {
+ time?: {
+ from?: string;
+ to?: string;
+ };
+ refreshInterval?: {
+ pause?: boolean | string;
+ value?: number | string;
+ };
}
-export function getRisonHref({
- location,
- pathname,
- hash,
- query = {}
-}: RisonHrefArgs) {
- const currentQuery = toQuery(location.search);
+export function getTimepickerRisonData(currentSearch: Location['search']) {
+ const currentQuery = toQuery(currentSearch);
const nextQuery = {
...TIMEPICKER_DEFAULTS,
- ...pick(currentQuery, PERSISTENT_APM_PARAMS),
- ...query
+ ...currentQuery
};
-
- // Create _g value for non-apm links
- const g = createG(nextQuery);
- const encodedG = rison.encode(g);
- const encodedA = query._a ? rison.encode(query._a) : ''; // TODO: Do we need to url-encode the _a values before rison encoding _a?
- const risonQuery: RisonEncoded = {
- _g: encodedG
+ return {
+ time: {
+ from: encodeURIComponent(nextQuery.rangeFrom),
+ to: encodeURIComponent(nextQuery.rangeTo)
+ },
+ refreshInterval: {
+ pause: String(nextQuery.refreshPaused),
+ value: String(nextQuery.refreshInterval)
+ }
};
-
- if (encodedA) {
- risonQuery._a = encodedA;
- }
-
- // don't URI-encode the already-encoded rison
- const search = qs.stringify(risonQuery, undefined, undefined, {
- encodeURIComponent: (v: string) => v
- });
-
- const href = url.format({
- pathname: chrome.addBasePath(pathname),
- hash: `${hash}?${search}`
- });
-
- return href;
}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx
index 78592abd130800..ff26ed93186951 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx
@@ -4,13 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Location } from 'history';
-import url from 'url';
// @ts-ignore
import { toJson } from '../testHelpers';
import {
fromQuery,
- getKibanaHref,
legacyDecodeURIComponent,
legacyEncodeURIComponent,
toQuery
@@ -58,59 +55,6 @@ describe('fromQuery', () => {
});
});
-describe('getKibanaHref', () => {
- it('should build correct URL for APM paths, merging in existing date range params', () => {
- const location = { search: '?rangeFrom=now/w&rangeTo=now-24h' } as Location;
- const pathname = '/app/apm';
- const hash = '/services/x/transactions';
- const query = { transactionId: 'something' };
- const href = getKibanaHref({ location, pathname, hash, query });
- expect(href).toEqual(
- '/app/apm#/services/x/transactions?rangeFrom=now%2Fw&rangeTo=now-24h&refreshPaused=true&refreshInterval=0&transactionId=something'
- );
- });
-
- it('should build correct url for non-APM paths, ignoring date range params', () => {
- const location = { search: '?rangeFrom=now/w&rangeTo=now-24h' } as Location;
- const pathname = '/app/kibana';
- const hash = '/outside';
- const query = { transactionId: 'something' };
- const href = getKibanaHref({ location, pathname, hash, query });
- expect(href).toEqual('/app/kibana#/outside?transactionId=something');
- });
-
- describe('when location contains kuery', () => {
- const location = {
- search: '?kuery=transaction.duration.us~20~3E~201'
- } as Location;
-
- it('should preserve kql for apm links', () => {
- const pathname = '/app/apm';
- const href = getKibanaHref({ location, pathname });
- const { kuery } = getUrlQuery(href);
- expect(kuery).toEqual('transaction.duration.us~20~3E~201');
- });
-
- it('should preserve kql for links without path', () => {
- const href = getKibanaHref({ location });
- const { kuery } = getUrlQuery(href);
- expect(kuery).toEqual('transaction.duration.us~20~3E~201');
- });
-
- it('should not preserve kql for non-apm links', () => {
- const pathname = '/app/kibana';
- const href = getKibanaHref({ location, pathname });
- const { kuery } = getUrlQuery(href);
- expect(kuery).toEqual(undefined);
- });
- });
-});
-
-function getUrlQuery(href: string) {
- const hash = url.parse(href).hash!.slice(1);
- return url.parse(hash, true).query;
-}
-
describe('legacyEncodeURIComponent', () => {
it('should encode a string with forward slashes', () => {
expect(legacyEncodeURIComponent('a/b/c')).toBe('a~2Fb~2Fc');
diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts
index f3831fcb9805a1..544a19ddab47fd 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts
+++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts
@@ -4,74 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Location } from 'history';
import createHistory from 'history/createHashHistory';
-import { pick } from 'lodash';
import qs from 'querystring';
-import chrome from 'ui/chrome';
-import url from 'url';
-import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
+import { StringMap } from '../../../../typings/common';
export function toQuery(search?: string): APMQueryParamsRaw {
return search ? qs.parse(search.slice(1)) : {};
}
-export function fromQuery(query: APMQueryParams) {
+export function fromQuery(query: StringMap) {
return qs.stringify(query);
}
-export const PERSISTENT_APM_PARAMS = [
- 'kuery',
- 'rangeFrom',
- 'rangeTo',
- 'refreshPaused',
- 'refreshInterval'
-];
-
-function getSearchString(
- location: Location,
- pathname: string,
- query: APMQueryParams = {}
-) {
- const currentQuery = toQuery(location.search);
-
- // Preserve existing params for apm links
- const isApmLink = pathname.includes('app/apm') || pathname === '';
- if (isApmLink) {
- const nextQuery = {
- ...TIMEPICKER_DEFAULTS,
- ...pick(currentQuery, PERSISTENT_APM_PARAMS),
- ...query
- };
- return fromQuery(nextQuery);
- }
-
- return fromQuery(query);
-}
-
-export interface KibanaHrefArgs {
- location: Location;
- pathname?: string;
- hash?: string;
- query?: T;
-}
-
-// TODO: Will eventually need to solve for the case when we need to use this helper to link to
-// another Kibana app which requires url query params not covered by APMQueryParams
-export function getKibanaHref({
- location,
- pathname = '',
- hash,
- query = {}
-}: KibanaHrefArgs): string {
- const search = getSearchString(location, pathname, query);
- const href = url.format({
- pathname: chrome.addBasePath(pathname),
- hash: `${hash}?${search}`
- });
- return href;
-}
-
export interface APMQueryParams {
transactionId?: string;
traceId?: string;
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
index 14ff7fe2b346a3..f86ecc5fb0f984 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
@@ -11,18 +11,14 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
- EuiLink,
EuiPopover
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { Location } from 'history';
import React from 'react';
import { idx } from '../../../../common/idx';
-import { StringMap } from '../../../../typings/common';
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
-import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink';
-import { QueryWithIndexPattern } from '../Links/DiscoverLinks/QueryWithIndexPattern';
-import { getRisonHref } from '../Links/rison_helpers';
+import { DiscoverTransactionLink } from '../Links/DiscoverLinks/DiscoverTransactionLink';
+import { InfraLink } from '../Links/InfraLink';
function getInfraMetricsQuery(transaction: Transaction) {
const plus5 = new Date(transaction['@timestamp']);
@@ -49,7 +45,6 @@ function ActionMenuButton({ onClick }: { onClick: () => void }) {
interface Props {
readonly transaction: Transaction;
- readonly location: Location;
}
interface State {
@@ -70,11 +65,11 @@ export class TransactionActionMenu extends React.Component {
};
public getInfraActions() {
- const { transaction, location } = this.props;
+ const { transaction } = this.props;
const hostName = idx(transaction, _ => _.host.hostname);
const podId = idx(transaction, _ => _.kubernetes.pod.uid);
const containerId = idx(transaction, _ => _.container.id);
- const pathname = '/app/infra';
+ const traceId = idx(transaction, _ => _.trace.id);
const time = new Date(transaction['@timestamp']).getTime();
const infraMetricsQuery = getInfraMetricsQuery(transaction);
@@ -85,80 +80,80 @@ export class TransactionActionMenu extends React.Component {
'xpack.apm.transactionActionMenu.showPodLogsLinkLabel',
{ defaultMessage: 'Show pod logs' }
),
- target: podId,
- hash: `/link-to/pod-logs/${podId}`,
+ condition: podId,
+ path: `/link-to/pod-logs/${podId}`,
query: { time }
},
-
{
icon: 'loggingApp',
label: i18n.translate(
'xpack.apm.transactionActionMenu.showContainerLogsLinkLabel',
{ defaultMessage: 'Show container logs' }
),
- target: containerId,
- hash: `/link-to/container-logs/${containerId}`,
+ condition: containerId,
+ path: `/link-to/container-logs/${containerId}`,
query: { time }
},
-
{
icon: 'loggingApp',
label: i18n.translate(
'xpack.apm.transactionActionMenu.showHostLogsLinkLabel',
{ defaultMessage: 'Show host logs' }
),
- target: hostName,
- hash: `/link-to/host-logs/${hostName}`,
+ condition: hostName,
+ path: `/link-to/host-logs/${hostName}`,
query: { time }
},
-
+ {
+ icon: 'loggingApp',
+ label: i18n.translate(
+ 'xpack.apm.transactionActionMenu.showTraceLogsLinkLabel',
+ { defaultMessage: 'Show trace logs' }
+ ),
+ target: traceId,
+ hash: `/link-to/logs`,
+ query: { time, filter: `trace.id:${traceId}` }
+ },
{
icon: 'infraApp',
label: i18n.translate(
'xpack.apm.transactionActionMenu.showPodMetricsLinkLabel',
{ defaultMessage: 'Show pod metrics' }
),
- target: podId,
- hash: `/link-to/pod-detail/${podId}`,
+ condition: podId,
+ path: `/link-to/pod-detail/${podId}`,
query: infraMetricsQuery
},
-
{
icon: 'infraApp',
label: i18n.translate(
'xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel',
{ defaultMessage: 'Show container metrics' }
),
- target: containerId,
- hash: `/link-to/container-detail/${containerId}`,
+ condition: containerId,
+ path: `/link-to/container-detail/${containerId}`,
query: infraMetricsQuery
},
-
{
icon: 'infraApp',
label: i18n.translate(
'xpack.apm.transactionActionMenu.showHostMetricsLinkLabel',
{ defaultMessage: 'Show host metrics' }
),
- target: hostName,
- hash: `/link-to/host-detail/${hostName}`,
+ condition: hostName,
+ path: `/link-to/host-detail/${hostName}`,
query: infraMetricsQuery
}
]
- .filter(({ target }) => Boolean(target))
- .map(({ icon, label, hash, query }, index) => {
- const href = getRisonHref({
- location,
- pathname,
- hash,
- query: query as StringMap // TODO: differentiate between APM ui query args, and external query args
- });
-
+ .filter(({ condition }) => Boolean(condition))
+ .map(({ icon, label, path, query }, index) => {
return (
-
+
- {label}
+
+ {label}
+
@@ -170,62 +165,46 @@ export class TransactionActionMenu extends React.Component {
}
public render() {
- const { transaction, location } = this.props;
- return (
-
- {query => {
- const discoverTransactionHref = getRisonHref({
- location,
- pathname: '/app/kibana',
- hash: '/discover',
- query
- });
-
- const items = [
- ...this.getInfraActions(),
-
-
-
-
- {i18n.translate(
- 'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel',
- {
- defaultMessage: 'View sample document'
- }
- )}
-
-
-
-
-
-
-
- ];
+ const { transaction } = this.props;
+
+ const items = [
+ ...this.getInfraActions(),
+
+
+
+
+ {i18n.translate(
+ 'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel',
+ {
+ defaultMessage: 'View sample document'
+ }
+ )}
+
+
+
+
+
+
+
+ ];
- return (
- }
- isOpen={this.state.isOpen}
- closePopover={this.close}
- anchorPosition="downRight"
- panelPaddingSize="none"
- >
-
-
- );
- }}
-
+ return (
+ }
+ isOpen={this.state.isOpen}
+ closePopover={this.close}
+ anchorPosition="downRight"
+ panelPaddingSize="none"
+ >
+
+
);
}
}
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx
index 9944385a70b190..b35b410ad3d08b 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx
@@ -8,14 +8,12 @@ import { shallow } from 'enzyme';
import 'jest-styled-components';
import React from 'react';
import { TransactionActionMenu } from '../TransactionActionMenu';
-import { location, transaction } from './mockData';
+import { transaction } from './mockData';
describe('TransactionActionMenu component', () => {
it('should render with data', () => {
expect(
- shallow(
-
- ).shallow()
+ shallow( ).shallow()
).toMatchSnapshot();
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap
index 760406df34616f..bc77dd131c77c5 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap
@@ -1,209 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TransactionActionMenu component should render with data 1`] = `
-
- }
- closePopover={[Function]}
- hasArrow={true}
- id="transactionActionMenu"
- isOpen={false}
- ownFocus={false}
- panelPaddingSize="none"
+
-
-
-
-
- Show pod logs
-
-
-
-
-
-
- ,
-
-
-
-
- Show container logs
-
-
-
-
-
-
- ,
-
-
-
-
- Show host logs
-
-
-
-
-
-
- ,
-
-
-
-
- Show pod metrics
-
-
-
-
-
-
- ,
-
-
-
-
- Show container metrics
-
-
-
-
-
-
- ,
-
-
-
-
- Show host metrics
-
-
-
-
-
-
- ,
-
-
-
-
- View sample document
-
-
-
-
-
-
- ,
- ]
- }
- title="Actions"
- />
-
+
+
`;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
index 333ca8bc089bdf..a2e5225e2db260 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
@@ -22,7 +22,7 @@ import { ITransactionChartData } from '../../../../store/selectors/chartSelector
import { IUrlParams } from '../../../../store/urlParams';
import { asInteger, asMillis, tpmUnit } from '../../../../utils/formatters';
import { LicenseContext } from '../../../app/Main/LicenseCheck';
-import { MLJobLink } from '../../Links/MLJobLink';
+import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
// @ts-ignore
import CustomPlot from '../CustomPlot';
import { SyncChartGroup } from '../SyncChartGroup';
@@ -112,7 +112,6 @@ export class TransactionCharts extends Component {
View Job
diff --git a/x-pack/plugins/apm/public/context/LocationContext.tsx b/x-pack/plugins/apm/public/context/LocationContext.tsx
new file mode 100644
index 00000000000000..88cd5cec811334
--- /dev/null
+++ b/x-pack/plugins/apm/public/context/LocationContext.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { History, Location } from 'history';
+import React, { createContext, useState } from 'react';
+
+interface Props {
+ history: History;
+}
+
+const initialLocation = {} as Location;
+
+const LocationContext = createContext(initialLocation);
+const LocationProvider: React.FC = ({ history, ...props }) => {
+ const [location, setLocation] = useState(history.location);
+ history.listen(updatedLocation => setLocation(updatedLocation));
+ return ;
+};
+
+export { LocationContext, LocationProvider };
diff --git a/x-pack/plugins/apm/public/hooks/useAPMIndexPattern.tsx b/x-pack/plugins/apm/public/hooks/useAPMIndexPattern.tsx
new file mode 100644
index 00000000000000..7b062b57d620d2
--- /dev/null
+++ b/x-pack/plugins/apm/public/hooks/useAPMIndexPattern.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useEffect, useState } from 'react';
+import {
+ getAPMIndexPattern,
+ ISavedObject
+} from '../services/rest/savedObjects';
+
+export function useAPMIndexPattern() {
+ const [pattern, setPattern] = useState({} as ISavedObject);
+
+ async function fetchPattern() {
+ const indexPattern = await getAPMIndexPattern();
+ if (indexPattern) {
+ setPattern(indexPattern);
+ }
+ }
+
+ useEffect(() => {
+ fetchPattern();
+ }, []);
+
+ return pattern;
+}
diff --git a/x-pack/plugins/infra/public/store/local/metric_time/index.ts b/x-pack/plugins/apm/public/hooks/useLocation.tsx
similarity index 54%
rename from x-pack/plugins/infra/public/store/local/metric_time/index.ts
rename to x-pack/plugins/apm/public/hooks/useLocation.tsx
index 1df7b682d1314e..e2b4e3e479629a 100644
--- a/x-pack/plugins/infra/public/store/local/metric_time/index.ts
+++ b/x-pack/plugins/apm/public/hooks/useLocation.tsx
@@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import * as metricTimeActions from './actions';
-import * as metricTimeSelectors from './selectors';
+import { useContext } from 'react';
+import { LocationContext } from '../context/LocationContext';
-export { metricTimeActions, metricTimeSelectors };
-export * from './epic';
-export * from './reducer';
+export function useLocation() {
+ return useContext(LocationContext);
+}
diff --git a/x-pack/plugins/apm/public/index.tsx b/x-pack/plugins/apm/public/index.tsx
index 3779776c54e0c9..ff5b7b679e2d20 100644
--- a/x-pack/plugins/apm/public/index.tsx
+++ b/x-pack/plugins/apm/public/index.tsx
@@ -19,6 +19,7 @@ import 'uiExports/autocompleteProviders';
import { GlobalHelpExtension } from './components/app/GlobalHelpExtension';
import { Main } from './components/app/Main';
import { history } from './components/shared/Links/url_helpers';
+import { LocationProvider } from './context/LocationContext';
// @ts-ignore
import configureStore from './store/config/configureStore';
import './style/global_overrides.css';
@@ -53,7 +54,9 @@ waitForRoot.then(() => {
-
+
+
+
,
diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx
index 3b720cba6e03de..2072fa69e66fd7 100644
--- a/x-pack/plugins/apm/public/utils/testHelpers.tsx
+++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx
@@ -6,19 +6,15 @@
/* global jest */
-import { mount, ReactWrapper } from 'enzyme';
+import { ReactWrapper } from 'enzyme';
import enzymeToJson from 'enzyme-to-json';
+import { History, Location } from 'history';
import 'jest-styled-components';
import moment from 'moment';
import { Moment } from 'moment-timezone';
import React from 'react';
-import { Provider } from 'react-redux';
-import { MemoryRouter } from 'react-router-dom';
-// @ts-ignore
-import { createMockStore } from 'redux-test-utils';
-// @ts-ignore
-import configureStore from '../store/config/configureStore';
-import { IReduxState } from '../store/rootReducer';
+import { render, waitForElement } from 'react-testing-library';
+import { LocationProvider } from '../context/LocationContext';
export function toJson(wrapper: ReactWrapper) {
return enzymeToJson(wrapper, {
@@ -44,22 +40,25 @@ export function mockMoment() {
}
// Useful for getting the rendered href from any kind of link component
-export async function getRenderedHref(
- Component: React.FunctionComponent<{}>,
- globalState: Partial = {}
-) {
- const store = configureStore(globalState);
- const mounted = mount(
-
-
-
-
-
+export async function getRenderedHref(Component: React.FC, location: Location) {
+ const el = render(
+
+
+
);
await tick();
+ await waitForElement(() => el.container.querySelector('a'));
- return mounted.render().attr('href');
+ const a = el.container.querySelector('a');
+ return a ? a.getAttribute('href') : '';
}
export function mockNow(date: string) {
diff --git a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap
index 47fbad75a1e5d7..0c5b0e346f8ff0 100644
--- a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap
@@ -56,7 +56,7 @@ Array [
"min": 100,
},
"field": "@timestamp",
- "interval": "1s",
+ "interval": "30s",
"min_doc_count": 0,
},
},
diff --git a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/fetcher.ts
index fa02a8fecbf5dc..9a6d07a0a6e4ec 100644
--- a/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/fetcher.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/get_cpu_chart_data/fetcher.ts
@@ -34,7 +34,7 @@ interface Aggs {
export type ESResponse = PromiseReturnType;
export async function fetch({ serviceName, setup }: MetricsRequestArgs) {
const { start, end, esFilterQuery, client, config } = setup;
- const { intervalString } = getBucketSize(start, end, 'auto');
+ const { bucketSize } = getBucketSize(start, end, 'auto');
const filters: ESFilter[] = [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [PROCESSOR_EVENT]: 'metric' } },
@@ -56,7 +56,9 @@ export async function fetch({ serviceName, setup }: MetricsRequestArgs) {
timeseriesData: {
date_histogram: {
field: '@timestamp',
- interval: intervalString,
+
+ // ensure minimum bucket size of 30s since this is the default resolution for metric data
+ interval: `${Math.max(bucketSize, 30)}s`,
min_doc_count: 0,
extended_bounds: { min: start, max: end }
},
diff --git a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap
index c5f1b36fef7573..064a4fa5d24b32 100644
--- a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/__tests__/__snapshots__/fetcher.test.ts.snap
@@ -48,7 +48,7 @@ Array [
"min": 100,
},
"field": "@timestamp",
- "interval": "1s",
+ "interval": "30s",
"min_doc_count": 0,
},
},
diff --git a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/fetcher.ts
index bea3244cb07e83..0bccff17aebe87 100644
--- a/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/fetcher.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/get_memory_chart_data/fetcher.ts
@@ -30,7 +30,7 @@ interface Aggs {
export type ESResponse = PromiseReturnType;
export async function fetch({ serviceName, setup }: MetricsRequestArgs) {
const { start, end, esFilterQuery, client, config } = setup;
- const { intervalString } = getBucketSize(start, end, 'auto');
+ const { bucketSize } = getBucketSize(start, end, 'auto');
const filters: ESFilter[] = [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [PROCESSOR_EVENT]: 'metric' } },
@@ -59,7 +59,9 @@ export async function fetch({ serviceName, setup }: MetricsRequestArgs) {
timeseriesData: {
date_histogram: {
field: '@timestamp',
- interval: intervalString,
+
+ // ensure minimum bucket size of 30s since this is the default resolution for metric data
+ interval: `${Math.max(bucketSize, 30)}s`,
min_doc_count: 0,
extended_bounds: { min: start, max: end }
},
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.js b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.js
index fc32e64e667eb3..48873757a43126 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.js
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.js
@@ -157,18 +157,13 @@ export class AssetManager extends React.PureComponent {
const percentageUsed = Math.round((assetsTotal / ASSET_MAX_SIZE) * 100);
const emptyAssets = (
-
+
No available assets}
- titleSize="s"
- body={
-
- Upload your assets above to get started
-
- }
+ title={Import your assets to get started }
+ titleSize="xs"
/>
-
+
);
const assetModal = isModalVisible ? (
@@ -201,9 +196,8 @@ export class AssetManager extends React.PureComponent {
- Below are the image assets that you added to this workpad. To reclaim space, delete
- assets that you no longer need. Unfortunately, any assets that are actually in use
- cannot be determined at this time.
+ Below are the image assets in this workpad. Any assets that are currently in use
+ cannot be determined at this time. To reclaim space, delete assets.
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.scss b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.scss
index 84534971d0ec57..652244d9f3a055 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.scss
+++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.scss
@@ -38,7 +38,7 @@
.canvasAssetManager__emptyPanel {
max-width: 400px;
- margin: 0 auto;
+ margin: $euiSizeXL auto 0;
}
.canvasAssetManager__thumb {
diff --git a/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js b/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js
index 77d1a8d3c2bc1c..3b6c76b9935f69 100644
--- a/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js
+++ b/x-pack/plugins/canvas/public/components/dom_preview/dom_preview.js
@@ -9,42 +9,53 @@ import PropTypes from 'prop-types';
import { debounce } from 'lodash';
export class DomPreview extends React.Component {
- static container = null;
- static content = null;
- static observer = null;
-
static propTypes = {
elementId: PropTypes.string.isRequired,
height: PropTypes.number.isRequired,
};
- componentDidMount() {
- const original = document.querySelector(`#${this.props.elementId}`);
-
- const update = this.update(original);
- update();
-
- const slowUpdate = debounce(update, 250);
+ constructor(props) {
+ super(props);
+ this.container = null;
+ this.content = null;
+ this.observer = null;
+ this.original = null;
+ this.updateTimeout = null;
+ }
- this.observer = new MutationObserver(slowUpdate);
- // configuration of the observer
- const config = { attributes: true, childList: true, subtree: true };
- // pass in the target node, as well as the observer options
- this.observer.observe(original, config);
+ componentDidMount() {
+ this.update();
}
componentWillUnmount() {
- this.observer.disconnect();
+ clearTimeout(this.updateTimeout);
+ this.observer && this.observer.disconnect(); // observer not guaranteed to exist
}
- update = original => () => {
+ update = () => {
if (!this.content || !this.container) {
return;
}
- const thumb = original.cloneNode(true);
+ if (!this.observer) {
+ this.original = this.original || document.querySelector(`#${this.props.elementId}`);
+ if (this.original) {
+ const slowUpdate = debounce(this.update, 100);
+ this.observer = new MutationObserver(slowUpdate);
+ // configuration of the observer
+ const config = { attributes: true, childList: true, subtree: true };
+ // pass in the target node, as well as the observer options
+ this.observer.observe(this.original, config);
+ } else {
+ clearTimeout(this.updateTimeout); // to avoid the assumption that we fully control when `update` is called
+ this.updateTimeout = setTimeout(this.update, 30);
+ return;
+ }
+ }
+
+ const thumb = this.original.cloneNode(true);
- const originalStyle = window.getComputedStyle(original, null);
+ const originalStyle = window.getComputedStyle(this.original, null);
const originalWidth = parseInt(originalStyle.getPropertyValue('width'), 10);
const originalHeight = parseInt(originalStyle.getPropertyValue('height'), 10);
@@ -61,7 +72,7 @@ export class DomPreview extends React.Component {
this.container.style.cssText = `width: ${thumbWidth}px; height: ${thumbHeight}px;`;
// Copy canvas data
- const originalCanvas = original.querySelectorAll('canvas');
+ const originalCanvas = this.original.querySelectorAll('canvas');
const thumbCanvas = thumb.querySelectorAll('canvas');
// Cloned canvas elements are blank and need to be explicitly redrawn
diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_controls.js b/x-pack/plugins/canvas/public/components/page_preview/page_controls.js
index 6cde599c79fa65..f04d44a74165ef 100644
--- a/x-pack/plugins/canvas/public/components/page_preview/page_controls.js
+++ b/x-pack/plugins/canvas/public/components/page_preview/page_controls.js
@@ -26,12 +26,8 @@ export const PageControls = ({ pageId, onDelete, onDuplicate }) => {
justifyContent="spaceBetween"
>
-
-
+
+
diff --git a/x-pack/plugins/canvas/public/components/refresh_control/index.js b/x-pack/plugins/canvas/public/components/refresh_control/index.js
index ca92e7da5c944d..0fb01850203572 100644
--- a/x-pack/plugins/canvas/public/components/refresh_control/index.js
+++ b/x-pack/plugins/canvas/public/components/refresh_control/index.js
@@ -8,13 +8,12 @@ import { connect } from 'react-redux';
import { fetchAllRenderables } from '../../state/actions/elements';
import { setRefreshInterval } from '../../state/actions/workpad';
import { getInFlight } from '../../state/selectors/resolved_args';
-import { getRefreshInterval, getElementStats } from '../../state/selectors/workpad';
+import { getRefreshInterval } from '../../state/selectors/workpad';
import { RefreshControl as Component } from './refresh_control';
const mapStateToProps = state => ({
inFlight: getInFlight(state),
refreshInterval: getRefreshInterval(state),
- elementStats: getElementStats(state),
});
const mapDispatchToProps = {
diff --git a/x-pack/plugins/canvas/public/components/refresh_control/refresh_control.js b/x-pack/plugins/canvas/public/components/refresh_control/refresh_control.js
index f3b09085aee54d..5d93538bc68e57 100644
--- a/x-pack/plugins/canvas/public/components/refresh_control/refresh_control.js
+++ b/x-pack/plugins/canvas/public/components/refresh_control/refresh_control.js
@@ -8,7 +8,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import { EuiButtonEmpty } from '@elastic/eui';
import { Popover } from '../popover';
-import { loadingIndicator } from '../../lib/loading_indicator';
import { AutoRefreshControls } from './auto_refresh_controls';
const getRefreshInterval = (val = '') => {
@@ -37,21 +36,7 @@ const getRefreshInterval = (val = '') => {
}
};
-export const RefreshControl = ({
- inFlight,
- elementStats,
- setRefreshInterval,
- refreshInterval,
- doRefresh,
-}) => {
- const { pending } = elementStats;
-
- if (inFlight || pending > 0) {
- loadingIndicator.show();
- } else {
- loadingIndicator.hide();
- }
-
+export const RefreshControl = ({ inFlight, setRefreshInterval, refreshInterval, doRefresh }) => {
const setRefresh = val => setRefreshInterval(getRefreshInterval(val));
const popoverButton = handleClick => (
diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar_component.js b/x-pack/plugins/canvas/public/components/sidebar/sidebar_component.js
index 41a4943c85dc86..3d82a61efd1c00 100644
--- a/x-pack/plugins/canvas/public/components/sidebar/sidebar_component.js
+++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar_component.js
@@ -104,15 +104,12 @@ export const SidebarComponent = ({
-
+
duplicateElement()}
- aria-label="Duplicate this element into a new layer"
+ aria-label="Clone the selected element"
/>
diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.js b/x-pack/plugins/canvas/public/components/toolbar/toolbar.js
index ad41219ca367f6..49d3367209c48a 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.js
+++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.js
@@ -56,7 +56,7 @@ export const Toolbar = props => {
- Dismiss
+ Close
diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.js b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.js
index bc8aa5e0dadec4..b0cb49c87ccea3 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.js
+++ b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.js
@@ -13,7 +13,7 @@ export const Tray = ({ children, done }) => {
-
+
diff --git a/x-pack/plugins/canvas/public/components/workpad_export/workpad_export.js b/x-pack/plugins/canvas/public/components/workpad_export/workpad_export.js
index bb2edd810e6a46..7ed7d410bca655 100644
--- a/x-pack/plugins/canvas/public/components/workpad_export/workpad_export.js
+++ b/x-pack/plugins/canvas/public/components/workpad_export/workpad_export.js
@@ -57,7 +57,7 @@ export class WorkpadExport extends React.PureComponent {
return (
- PDFs can take a minute or two to generate based upon the size of your workpad
+ PDFs can take a minute or two to generate based on the size of your workpad.
@@ -95,7 +95,7 @@ export class WorkpadExport extends React.PureComponent {
iconType="copy"
size="s"
style={{ width: '100%' }}
- aria-label="Alternatively, you can generate a PDF from a script or with Watcher by using this URL. Hit Enter to copy the URL to clipboard"
+ aria-label="Alternatively, you can generate a PDF from a script or with Watcher by using this URL. Press Enter to copy the URL to clipboard."
>
Copy POST URL
@@ -117,11 +117,11 @@ export class WorkpadExport extends React.PureComponent {
},
},
{
- name: 'PDF Reports',
+ name: 'PDF reports',
icon: 'document',
panel: {
id: 1,
- title: 'PDF Reports',
+ title: 'PDF reports',
content: this.props.enabled
? this.renderPDFControls(closePopover)
: this.renderDisabled(),
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js
index 846c3f1ff5b1ed..173b346143cfd5 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js
+++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js
@@ -67,7 +67,7 @@ export class WorkpadHeader extends React.PureComponent {
/>
setShowElementModal(false)}>
- Dismiss
+ Close
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js
index 44baa61e0c221f..7eb3edaa296ea6 100644
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js
+++ b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js
@@ -146,11 +146,11 @@ export class WorkpadLoader extends React.PureComponent {
render: workpad => (
-
+
this.props.downloadWorkpad(workpad.id)}
- aria-label="Download Workpad"
+ aria-label="Export workpad"
/>
@@ -174,7 +174,7 @@ export class WorkpadLoader extends React.PureComponent {
const columns = [
{
field: 'name',
- name: 'Workpad Name',
+ name: 'Workpad name',
sortable: true,
dataType: 'string',
render: (name, workpad) => {
@@ -294,7 +294,7 @@ export class WorkpadLoader extends React.PureComponent {
const downloadButton = (
- {`Download (${selectedWorkpads.length})`}
+ {`Export (${selectedWorkpads.length})`}
);
diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js
index 3991d4286750f9..42552c2b8d990e 100644
--- a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js
+++ b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js
@@ -22,7 +22,7 @@ export const WorkpadManager = ({ onClose }) => {
const tabs = [
{
id: 'workpadLoader',
- name: 'My Workpads',
+ name: 'My workpads',
content: (
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/index.js b/x-pack/plugins/canvas/public/components/workpad_templates/index.js
index f6f16119c78a8c..cb4e6cc1e6fc5f 100644
--- a/x-pack/plugins/canvas/public/components/workpad_templates/index.js
+++ b/x-pack/plugins/canvas/public/components/workpad_templates/index.js
@@ -25,7 +25,7 @@ export const WorkpadTemplates = compose(
// Clone workpad given an id
cloneWorkpad: props => workpad => {
workpad.id = getId('workpad');
- workpad.name = `Untitled Workpad - ${workpad.name}`;
+ workpad.name = `My Canvas Workpad - ${workpad.name}`;
workpad.tags = undefined;
return workpadService
.create(workpad)
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.js b/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.js
index ae50b5a843da58..ea2b50b317cf71 100644
--- a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.js
+++ b/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.js
@@ -73,7 +73,7 @@ export class WorkpadTemplates extends React.PureComponent {
const columns = [
{
field: 'name',
- name: 'Template Name',
+ name: 'Template name',
sortable: true,
width: '30%',
dataType: 'string',
diff --git a/x-pack/plugins/canvas/public/lib/loading_indicator.ts b/x-pack/plugins/canvas/public/lib/loading_indicator.ts
index 752f30820238cc..ee69aa24df55ff 100644
--- a/x-pack/plugins/canvas/public/lib/loading_indicator.ts
+++ b/x-pack/plugins/canvas/public/lib/loading_indicator.ts
@@ -9,6 +9,11 @@ import { loadingCount } from 'ui/chrome';
let isActive = false;
+export interface LoadingIndicatorInterface {
+ show: () => void;
+ hide: () => void;
+}
+
export const loadingIndicator = {
show: () => {
if (!isActive) {
diff --git a/x-pack/plugins/canvas/public/state/actions/resolved_args.js b/x-pack/plugins/canvas/public/state/actions/resolved_args.js
deleted file mode 100644
index bf77fe4b3af1d1..00000000000000
--- a/x-pack/plugins/canvas/public/state/actions/resolved_args.js
+++ /dev/null
@@ -1,16 +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 { createAction } from 'redux-actions';
-
-export const setLoading = createAction('setResolvedLoading');
-export const setValue = createAction('setResolvedValue');
-export const setValues = createAction('setResolvedValues');
-export const clearValue = createAction('clearResolvedValue');
-export const clearValues = createAction('clearResolvedValues');
-
-export const inFlightActive = createAction('inFlightActive');
-export const inFlightComplete = createAction('inFlightComplete');
diff --git a/x-pack/plugins/canvas/public/state/actions/resolved_args.ts b/x-pack/plugins/canvas/public/state/actions/resolved_args.ts
new file mode 100644
index 00000000000000..d680f151b915db
--- /dev/null
+++ b/x-pack/plugins/canvas/public/state/actions/resolved_args.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { Action } from 'redux';
+import { createAction } from 'redux-actions';
+
+export const setLoadingActionType = 'setResolvedLoading';
+export const setValueActionType = 'setResolvedValue';
+export const inFlightActiveActionType = 'inFlightActive';
+export const inFlightCompleteActionType = 'inFlightComplete';
+
+type InFlightActive = Action;
+type InFlightComplete = Action;
+
+interface SetResolvedLoadingPayload {
+ path: any[];
+}
+type SetResolvedLoading = Action & {
+ payload: SetResolvedLoadingPayload;
+};
+
+interface SetResolvedValuePayload {
+ path: any[];
+ value: any;
+}
+type SetResolvedValue = Action & {
+ payload: SetResolvedValuePayload;
+};
+
+export type Action = SetResolvedLoading | SetResolvedValue | InFlightActive | InFlightComplete;
+
+export const setLoading = createAction(setLoadingActionType);
+export const setValue = createAction(setValueActionType);
+export const setValues = createAction('setResolvedValues');
+export const clearValue = createAction('clearResolvedValue');
+export const clearValues = createAction('clearResolvedValues');
+
+export const inFlightActive = createAction(inFlightActiveActionType, () => undefined);
+export const inFlightComplete = createAction(
+ inFlightCompleteActionType,
+ () => undefined
+);
diff --git a/x-pack/plugins/canvas/public/state/defaults.js b/x-pack/plugins/canvas/public/state/defaults.js
index 203523936ffa78..13ff7102bcafe9 100644
--- a/x-pack/plugins/canvas/public/state/defaults.js
+++ b/x-pack/plugins/canvas/public/state/defaults.js
@@ -42,7 +42,7 @@ export const getDefaultPage = () => {
export const getDefaultWorkpad = () => {
const page = getDefaultPage();
return {
- name: 'Untitled Workpad',
+ name: 'My Canvas Workpad',
id: getId('workpad'),
width: 1080,
height: 720,
diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/in_flight.test.ts b/x-pack/plugins/canvas/public/state/middleware/__tests__/in_flight.test.ts
new file mode 100644
index 00000000000000..b78c3ffcfcb1da
--- /dev/null
+++ b/x-pack/plugins/canvas/public/state/middleware/__tests__/in_flight.test.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ inFlightActive,
+ inFlightComplete,
+ setLoading,
+ setValue,
+} from '../../actions/resolved_args';
+import { inFlightMiddlewareFactory } from '../in_flight';
+
+const next = jest.fn();
+const dispatch = jest.fn();
+const loadingIndicator = {
+ show: jest.fn(),
+ hide: jest.fn(),
+};
+const pendingCache: string[] = [];
+
+const testMiddleware = inFlightMiddlewareFactory({
+ loadingIndicator,
+ pendingCache,
+})({ dispatch, getState: jest.fn() })(next);
+
+describe('inflight middleware', () => {
+ beforeEach(() => {
+ dispatch.mockClear();
+ });
+
+ describe('loading indicator', () => {
+ beforeEach(() => {
+ loadingIndicator.show = jest.fn();
+ loadingIndicator.hide = jest.fn();
+ });
+
+ it('shows the loading indicator on inFlightActive action', () => {
+ const inFlightActiveAction = inFlightActive();
+
+ testMiddleware(inFlightActiveAction);
+
+ expect(loadingIndicator.show).toBeCalled();
+ });
+
+ it('hides the loading indicator on inFlightComplete action', () => {
+ const inFlightCompleteAction = inFlightComplete();
+
+ testMiddleware(inFlightCompleteAction);
+ expect(loadingIndicator.hide).toBeCalled();
+ });
+
+ describe('value', () => {
+ beforeEach(() => {
+ while (pendingCache.length) {
+ pendingCache.pop();
+ }
+ });
+
+ it('dispatches the inFlightAction for loadingValue actions', () => {
+ const path = ['some', 'path'];
+ const loadingAction = setLoading({ path });
+
+ testMiddleware(loadingAction);
+
+ expect(dispatch).toBeCalledWith(inFlightActive());
+ });
+
+ it('adds path to pendingCache for loadingValue actions', () => {
+ const expectedPath = 'path';
+ const path = [expectedPath];
+ const loadingAction = setLoading({ path });
+
+ testMiddleware(loadingAction);
+
+ expect(pendingCache[0]).toBe(expectedPath);
+ });
+
+ it('dispatches inFlight complete if all pending is resolved', () => {
+ const resolvedPath1 = 'path1';
+ const resolvedPath2 = 'path2';
+
+ const setAction1 = setValue({ path: [resolvedPath1], value: {} });
+ const setAction2 = setValue({ path: [resolvedPath2], value: {} });
+
+ pendingCache.push(resolvedPath1);
+ pendingCache.push(resolvedPath2);
+
+ testMiddleware(setAction1);
+ expect(dispatch).not.toBeCalled();
+
+ testMiddleware(setAction2);
+ expect(dispatch).toBeCalledWith(inFlightComplete());
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/canvas/public/state/middleware/in_flight.js b/x-pack/plugins/canvas/public/state/middleware/in_flight.js
deleted file mode 100644
index 2e48ee5b72621b..00000000000000
--- a/x-pack/plugins/canvas/public/state/middleware/in_flight.js
+++ /dev/null
@@ -1,35 +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 { convert } from '../../lib/modify_path';
-import { setLoading, setValue, inFlightActive, inFlightComplete } from '../actions/resolved_args';
-
-export const inFlight = ({ dispatch }) => next => {
- const pendingCache = [];
-
- return action => {
- const isLoading = action.type === setLoading.toString();
- const isSetting = action.type === setValue.toString();
-
- if (isLoading || isSetting) {
- const cacheKey = convert(action.payload.path).join('/');
-
- if (isLoading) {
- pendingCache.push(cacheKey);
- dispatch(inFlightActive());
- } else if (isSetting) {
- const idx = pendingCache.indexOf(cacheKey);
- pendingCache.splice(idx, 1);
- if (pendingCache.length === 0) {
- dispatch(inFlightComplete());
- }
- }
- }
-
- // execute the action
- next(action);
- };
-};
diff --git a/x-pack/plugins/canvas/public/state/middleware/in_flight.ts b/x-pack/plugins/canvas/public/state/middleware/in_flight.ts
new file mode 100644
index 00000000000000..7ad6f8aee15ed3
--- /dev/null
+++ b/x-pack/plugins/canvas/public/state/middleware/in_flight.ts
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Dispatch, Middleware } from 'redux';
+import {
+ loadingIndicator as defaultLoadingIndicator,
+ LoadingIndicatorInterface,
+} from '../../lib/loading_indicator';
+// @ts-ignore
+import { convert } from '../../lib/modify_path';
+
+interface InFlightMiddlewareOptions {
+ pendingCache: string[];
+ loadingIndicator: LoadingIndicatorInterface;
+}
+
+import {
+ Action as AnyAction,
+ inFlightActive,
+ inFlightActiveActionType,
+ inFlightComplete,
+ inFlightCompleteActionType,
+ setLoadingActionType,
+ setValueActionType,
+} from '../actions/resolved_args';
+
+const pathToKey = (path: any[]) => convert(path).join('/');
+
+export const inFlightMiddlewareFactory = ({
+ loadingIndicator,
+ pendingCache,
+}: InFlightMiddlewareOptions): Middleware => {
+ return ({ dispatch }) => (next: Dispatch) => {
+ return (action: AnyAction) => {
+ if (action.type === setLoadingActionType) {
+ const cacheKey = pathToKey(action.payload.path);
+ pendingCache.push(cacheKey);
+ dispatch(inFlightActive());
+ } else if (action.type === setValueActionType) {
+ const cacheKey = pathToKey(action.payload.path);
+ const idx = pendingCache.indexOf(cacheKey);
+ if (idx >= 0) {
+ pendingCache.splice(idx, 1);
+ }
+ if (pendingCache.length === 0) {
+ dispatch(inFlightComplete());
+ }
+ } else if (action.type === inFlightActiveActionType) {
+ loadingIndicator.show();
+ } else if (action.type === inFlightCompleteActionType) {
+ loadingIndicator.hide();
+ }
+
+ // execute the action
+ next(action);
+ };
+ };
+};
+
+export const inFlight = inFlightMiddlewareFactory({
+ loadingIndicator: defaultLoadingIndicator,
+ pendingCache: [],
+});
diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js
new file mode 100644
index 00000000000000..90664f07be6212
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js
@@ -0,0 +1,275 @@
+/*
+ * 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 from 'sinon';
+
+import { initTestBed, registerHttpRequestMockHelpers, nextTick, findTestSubject, getRandomString } from './test_helpers';
+import { AutoFollowPatternAdd } from '../../public/app/sections/auto_follow_pattern_add';
+import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from '../../../../../src/legacy/ui/public/index_patterns';
+import routing from '../../public/app/services/routing';
+
+jest.mock('ui/chrome', () => ({
+ addBasePath: (path) => path || 'api/cross_cluster_replication',
+ breadcrumbs: { set: () => {} },
+}));
+
+jest.mock('ui/index_patterns', () => {
+ const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } =
+ jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants');
+ const { validateIndexPattern, ILLEGAL_CHARACTERS, CONTAINS_SPACES } =
+ jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/validate/validate_index_pattern');
+ return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, validateIndexPattern, ILLEGAL_CHARACTERS, CONTAINS_SPACES };
+});
+
+const testBedOptions = {
+ memoryRouter: {
+ onRouter: (router) => routing.reactRouter = router
+ }
+};
+
+describe('Create Auto-follow pattern', () => {
+ let server;
+ let find;
+ let exists;
+ let component;
+ let getUserActions;
+ let form;
+ let getFormErrorsMessages;
+ let clickSaveForm;
+ let setLoadRemoteClustersResponse;
+
+ beforeEach(() => {
+ server = sinon.fakeServer.create();
+ server.respondImmediately = true;
+
+ // Register helpers to mock Http Requests
+ ({
+ setLoadRemoteClustersResponse
+ } = registerHttpRequestMockHelpers(server));
+
+ // Set "default" mock responses by not providing any arguments
+ setLoadRemoteClustersResponse();
+
+ // Mock all HTTP Requests that have not been handled previously
+ server.respondWith([200, {}, '']);
+ });
+
+ describe('on component mount', () => {
+ beforeEach(() => {
+ ({ find, exists } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions));
+ });
+
+ test('should display a "loading remote clusters" indicator', () => {
+ expect(exists('remoteClustersLoading')).toBe(true);
+ expect(find('remoteClustersLoading').text()).toBe('Loading remote clusters…');
+ });
+
+ test('should have a link to the documentation', () => {
+ expect(exists('autoFollowPatternDocsButton')).toBe(true);
+ });
+ });
+
+ describe('when remote clusters are loaded', () => {
+ beforeEach(async () => {
+ ({ find, exists, component, getUserActions, getFormErrorsMessages } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions));
+
+ ({ clickSaveForm } = getUserActions('autoFollowPatternForm'));
+
+ await nextTick(); // We need to wait next tick for the mock server response to comes in
+ component.update();
+ });
+
+ test('should display the Auto-follow pattern form', async () => {
+ expect(exists('ccrAutoFollowPatternForm')).toBe(true);
+ });
+
+ test('should display errors and disable the save button when clicking "save" without filling the form', () => {
+ expect(exists('autoFollowPatternFormError')).toBe(false);
+ expect(find('ccrAutoFollowPatternFormSubmitButton').props().disabled).toBe(false);
+
+ clickSaveForm();
+
+ expect(exists('autoFollowPatternFormError')).toBe(true);
+ expect(getFormErrorsMessages()).toEqual([
+ 'Name is required.',
+ 'At least one leader index pattern is required.',
+ ]);
+ expect(find('ccrAutoFollowPatternFormSubmitButton').props().disabled).toBe(true);
+ });
+ });
+
+ describe('form validation', () => {
+ describe('auto-follow pattern name', () => {
+ beforeEach(async () => {
+ ({ component, form, getUserActions, getFormErrorsMessages } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions));
+ ({ clickSaveForm } = getUserActions('autoFollowPatternForm'));
+
+ await nextTick();
+ component.update();
+ });
+
+ test('should not allow spaces', () => {
+ form.setInputValue('ccrAutoFollowPatternFormNameInput', 'with space');
+ clickSaveForm();
+ expect(getFormErrorsMessages()).toContain('Spaces are not allowed in the name.');
+ });
+
+ test('should not allow a "_" (underscore) as first character', () => {
+ form.setInputValue('ccrAutoFollowPatternFormNameInput', '_withUnderscore');
+ clickSaveForm();
+ expect(getFormErrorsMessages()).toContain(`Name can't begin with an underscore.`);
+ });
+
+ test('should not allow a "," (comma)', () => {
+ form.setInputValue('ccrAutoFollowPatternFormNameInput', 'with,coma');
+ clickSaveForm();
+ expect(getFormErrorsMessages()).toContain(`Commas are not allowed in the name.`);
+ });
+ });
+
+ describe('remote clusters', () => {
+ describe('when no remote clusters were found', () => {
+ test('should indicate it and have a button to add one', async () => {
+ setLoadRemoteClustersResponse([]);
+
+ ({ find, component } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions));
+ await nextTick();
+ component.update();
+ const errorCallOut = find('remoteClusterFieldNoClusterFoundError');
+
+ expect(errorCallOut.length).toBe(1);
+ expect(findTestSubject(errorCallOut, 'ccrRemoteClusterAddButton').length).toBe(1);
+ });
+ });
+
+ describe('when there was an error loading the remote clusters', () => {
+ test('should indicate no clusters found and have a button to add one', async () => {
+ setLoadRemoteClustersResponse(undefined, { body: 'Houston we got a problem' });
+
+ ({ find, component } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions));
+ await nextTick();
+ component.update();
+ const errorCallOut = find('remoteClusterFieldNoClusterFoundError');
+
+ expect(errorCallOut.length).toBe(1);
+ expect(findTestSubject(errorCallOut, 'ccrRemoteClusterAddButton').length).toBe(1);
+ });
+ });
+
+ describe('when none of the remote clusters is connected', () => {
+ const clusterName = 'new-york';
+ const remoteClusters = [{
+ name: clusterName,
+ seeds: ['localhost:9600'],
+ isConnected: false,
+ }];
+
+ beforeEach(async () => {
+ setLoadRemoteClustersResponse(remoteClusters);
+
+ ({ find, exists, component } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions));
+ await nextTick();
+ component.update();
+ });
+
+ test('should show a callout warning and have a button to edit the cluster', () => {
+ const errorCallOut = find('remoteClusterFieldCallOutError');
+
+ expect(errorCallOut.length).toBe(1);
+ expect(errorCallOut.find('.euiCallOutHeader__title').text()).toBe(`Remote cluster '${clusterName}' is not connected`);
+ expect(findTestSubject(errorCallOut, 'ccrRemoteClusterEditButton').length).toBe(1);
+ });
+
+ test('should have a button to add another remote cluster', () => {
+ expect(exists('ccrRemoteClusterInlineAddButton')).toBe(true);
+ });
+
+ test('should indicate in the select option that the cluster is not connected', () => {
+ const selectOptions = find('ccrRemoteClusterSelect').find('option');
+ expect(selectOptions.at(0).text()).toBe(`${clusterName} (not connected)`);
+ });
+ });
+ });
+
+ describe('index patterns', () => {
+ beforeEach(async () => {
+ ({ component, form, getUserActions, getFormErrorsMessages } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions));
+ ({ clickSaveForm } = getUserActions('autoFollowPatternForm'));
+
+ await nextTick();
+ component.update();
+ });
+
+ test('should not allow spaces', () => {
+ expect(getFormErrorsMessages()).toEqual([]);
+
+ form.setIndexPatternValue('with space');
+
+ expect(getFormErrorsMessages()).toContain('Spaces are not allowed in the index pattern.');
+ });
+
+ test('should not allow invalid characters', () => {
+ const expectInvalidChar = (char) => {
+ form.setIndexPatternValue(`with${char}space`);
+ expect(getFormErrorsMessages()).toContain(`Remove the character ${char} from the index pattern.`);
+ };
+
+ return INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.reduce((promise, char) => {
+ return promise.then(() => expectInvalidChar(char));
+ }, Promise.resolve());
+ });
+ });
+ });
+
+ describe('generated indices preview', () => {
+ beforeEach(async () => {
+ ({ exists, find, component, form, getUserActions } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions));
+ ({ clickSaveForm } = getUserActions('autoFollowPatternForm'));
+
+ await nextTick();
+ component.update();
+ });
+
+ test('should display a preview of the possible indices generated by the auto-follow pattern', () => {
+ expect(exists('ccrAutoFollowPatternIndicesPreview')).toBe(false);
+
+ form.setIndexPatternValue('kibana-');
+
+ expect(exists('ccrAutoFollowPatternIndicesPreview')).toBe(true);
+ });
+
+ test('should display 3 indices example when providing a wildcard(*)', () => {
+ form.setIndexPatternValue('kibana-*');
+ const indicesPreview = find('ccrAutoFollowPatternIndexPreview');
+
+ expect(indicesPreview.length).toBe(3);
+ expect(indicesPreview.at(0).text()).toContain('kibana-');
+ });
+
+ test('should only display 1 index example when *not* providing a wildcard', () => {
+ form.setIndexPatternValue('kibana');
+ const indicesPreview = find('ccrAutoFollowPatternIndexPreview');
+
+ expect(indicesPreview.length).toBe(1);
+ expect(indicesPreview.at(0).text()).toEqual('kibana');
+ });
+
+ test('should add the prefix and the suffix to the preview', () => {
+ const prefix = getRandomString();
+ const suffix = getRandomString();
+
+ form.setIndexPatternValue('kibana');
+ form.setInputValue('ccrAutoFollowPatternFormPrefixInput', prefix);
+ form.setInputValue('ccrAutoFollowPatternFormSuffixInput', suffix);
+
+ const indicesPreview = find('ccrAutoFollowPatternIndexPreview');
+ const textPreview = indicesPreview.at(0).text();
+
+ expect(textPreview).toContain(prefix);
+ expect(textPreview).toContain(suffix);
+ });
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js
new file mode 100644
index 00000000000000..698a20cbd8f08b
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js
@@ -0,0 +1,144 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import sinon from 'sinon';
+
+import { initTestBed, registerHttpRequestMockHelpers, nextTick, findTestSubject } from './test_helpers';
+import { AutoFollowPatternAdd } from '../../public/app/sections/auto_follow_pattern_add';
+import { AutoFollowPatternEdit } from '../../public/app/sections/auto_follow_pattern_edit';
+import { AutoFollowPatternForm } from '../../public/app/components/auto_follow_pattern_form';
+import routing from '../../public/app/services/routing';
+
+jest.mock('ui/chrome', () => ({
+ addBasePath: (path) => path || 'api/cross_cluster_replication',
+ breadcrumbs: { set: () => {} },
+}));
+
+jest.mock('ui/index_patterns', () => {
+ const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } =
+ jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants');
+ const { validateIndexPattern, ILLEGAL_CHARACTERS, CONTAINS_SPACES } =
+ jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/validate/validate_index_pattern');
+ return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, validateIndexPattern, ILLEGAL_CHARACTERS, CONTAINS_SPACES };
+});
+
+const AUTO_FOLLOW_PATTERN_NAME = 'my-autofollow';
+
+const AUTO_FOLLOW_PATTERN = {
+ name: AUTO_FOLLOW_PATTERN_NAME,
+ remoteCluster: 'cluster-2',
+ leaderIndexPatterns: ['my-pattern-*'],
+ followIndexPattern: 'prefix_{{leader_index}}_suffix'
+};
+
+const testBedOptions = {
+ memoryRouter: {
+ onRouter: (router) => routing.reactRouter = router,
+ // The auto-follow pattern id to fetch is read from the router ":id" param
+ // so we first set it in our initial entries
+ initialEntries: [`/${AUTO_FOLLOW_PATTERN_NAME}`],
+ // and then we declarae the :id param on the component route path
+ componentRoutePath: '/:id'
+ }
+};
+
+describe('Edit Auto-follow pattern', () => {
+ let server;
+ let find;
+ let component;
+ let getUserActions;
+ let getFormErrorsMessages;
+ let clickSaveForm;
+ let setLoadRemoteClustersResponse;
+ let setGetAutoFollowPatternResponse;
+
+ beforeEach(() => {
+ server = sinon.fakeServer.create();
+ server.respondImmediately = true;
+
+ // Register helpers to mock Http Requests
+ ({
+ setLoadRemoteClustersResponse,
+ setGetAutoFollowPatternResponse
+ } = registerHttpRequestMockHelpers(server));
+
+ // Set "default" mock responses by not providing any arguments
+ setLoadRemoteClustersResponse();
+
+ // Mock all HTTP Requests that have not been handled previously
+ server.respondWith([200, {}, '']);
+ });
+
+ describe('on component mount', () => {
+ const remoteClusters = [
+ { name: 'cluster-1', seeds: ['localhost:123'], isConnected: true },
+ { name: 'cluster-2', seeds: ['localhost:123'], isConnected: true },
+ ];
+
+ beforeEach(async () => {
+ setLoadRemoteClustersResponse(remoteClusters);
+ setGetAutoFollowPatternResponse(AUTO_FOLLOW_PATTERN);
+ ({ component, find } = initTestBed(AutoFollowPatternEdit, undefined, testBedOptions));
+
+ await nextTick();
+ component.update();
+ });
+
+ /**
+ * As the "edit" auto-follow pattern component uses the same form underneath that
+ * the "create" auto-follow pattern, we won't test it again but simply make sure that
+ * the form component is indeed shared between the 2 app sections.
+ */
+ test('should use the same Form component as the " " component', async () => {
+ const { component: addAutofollowPatternComponent } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions);
+
+ await nextTick();
+ addAutofollowPatternComponent.update();
+
+ const formEdit = component.find(AutoFollowPatternForm);
+ const formAdd = addAutofollowPatternComponent.find(AutoFollowPatternForm);
+
+ expect(formEdit.length).toBe(1);
+ expect(formAdd.length).toBe(1);
+ });
+
+ test('should populate the form fields with the values from the auto-follow pattern loaded', () => {
+ expect(find('ccrAutoFollowPatternFormNameInput').props().value).toBe(AUTO_FOLLOW_PATTERN.name);
+ expect(find('ccrRemoteClusterInput').props().value).toBe(AUTO_FOLLOW_PATTERN.remoteCluster);
+ expect(find('ccrAutoFollowPatternFormIndexPatternInput').text()).toBe(AUTO_FOLLOW_PATTERN.leaderIndexPatterns.join(''));
+ expect(find('ccrAutoFollowPatternFormPrefixInput').props().value).toBe('prefix_');
+ expect(find('ccrAutoFollowPatternFormSuffixInput').props().value).toBe('_suffix');
+ });
+ });
+
+ describe('when the remote cluster is disconnected', () => {
+ beforeEach(async () => {
+ setLoadRemoteClustersResponse([{ name: 'cluster-2', seeds: ['localhost:123'], isConnected: false }]);
+ setGetAutoFollowPatternResponse(AUTO_FOLLOW_PATTERN);
+ ({ component, find, getUserActions, getFormErrorsMessages } = initTestBed(AutoFollowPatternEdit, undefined, testBedOptions));
+ ({ clickSaveForm } = getUserActions('autoFollowPatternForm'));
+
+ await nextTick();
+ component.update();
+ });
+
+ test('should display an error and have a button to edit the remote cluster', () => {
+ const error = find('remoteClusterFieldCallOutError');
+
+ expect(error.length).toBe(1);
+ expect(error.find('.euiCallOutHeader__title').text())
+ .toBe(`Can't edit auto-follow pattern because remote cluster '${AUTO_FOLLOW_PATTERN.remoteCluster}' is not connected`);
+ expect(findTestSubject(error, 'ccrRemoteClusterEditButton').length).toBe(1);
+ });
+
+ test('should prevent saving the form and display an error message for the required remote cluster', () => {
+ clickSaveForm();
+
+ expect(getFormErrorsMessages()).toEqual(['A connected remote cluster is required.']);
+ expect(find('ccrAutoFollowPatternFormSubmitButton').props().disabled).toBe(true);
+ });
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js
index 365903729e81f0..3e95e457ce5d44 100644
--- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js
+++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js
@@ -16,7 +16,8 @@ jest.mock('ui/chrome', () => ({
}));
jest.mock('ui/index_patterns', () => {
- const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } = require.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants'); // eslint-disable-line max-len
+ const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } =
+ require.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants');
return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE };
});
diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js
new file mode 100644
index 00000000000000..f7617bb7973820
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js
@@ -0,0 +1,303 @@
+/*
+ * 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 from 'sinon';
+
+import { initTestBed, registerHttpRequestMockHelpers, nextTick } from './test_helpers';
+import { FollowerIndexAdd } from '../../public/app/sections/follower_index_add';
+import { AutoFollowPatternAdd } from '../../public/app/sections/auto_follow_pattern_add';
+import { RemoteClustersFormField } from '../../public/app/components';
+
+import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from '../../../../../src/legacy/ui/public/index_patterns';
+import routing from '../../public/app/services/routing';
+
+jest.mock('ui/chrome', () => ({
+ addBasePath: (path) => path || 'api/cross_cluster_replication',
+ breadcrumbs: { set: () => {} },
+}));
+
+jest.mock('ui/index_patterns', () => {
+ const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } =
+ jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants');
+ const { validateIndexPattern, ILLEGAL_CHARACTERS, CONTAINS_SPACES } =
+ jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/validate/validate_index_pattern');
+ return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, validateIndexPattern, ILLEGAL_CHARACTERS, CONTAINS_SPACES };
+});
+
+const testBedOptions = {
+ memoryRouter: {
+ onRouter: (router) => routing.reactRouter = router
+ }
+};
+
+describe('Create Follower index', () => {
+ let server;
+ let find;
+ let exists;
+ let component;
+ let getUserActions;
+ let form;
+ let getFormErrorsMessages;
+ let clickSaveForm;
+ let toggleAdvancedSettings;
+ let setLoadRemoteClustersResponse;
+
+ beforeEach(() => {
+ server = sinon.fakeServer.create();
+ server.respondImmediately = true;
+
+ // Register helpers to mock Http Requests
+ ({
+ setLoadRemoteClustersResponse,
+ } = registerHttpRequestMockHelpers(server));
+
+ // Set "default" mock responses by not providing any arguments
+ setLoadRemoteClustersResponse();
+
+ // Mock all HTTP Requests that have not been handled previously
+ server.respondWith([200, {}, '']);
+ });
+
+ describe('on component mount', () => {
+ beforeEach(() => {
+ ({ find, exists } = initTestBed(FollowerIndexAdd, undefined, testBedOptions));
+ });
+
+ test('should display a "loading remote clusters" indicator', () => {
+ expect(exists('remoteClustersLoading')).toBe(true);
+ expect(find('remoteClustersLoading').text()).toBe('Loading remote clusters…');
+ });
+
+ test('should have a link to the documentation', () => {
+ expect(exists('followerIndexDocsButton')).toBe(true);
+ });
+ });
+
+ describe('when remote clusters are loaded', () => {
+ beforeEach(async () => {
+ ({ find, exists, component, getUserActions, getFormErrorsMessages } = initTestBed(FollowerIndexAdd, undefined, testBedOptions));
+
+ ({ clickSaveForm } = getUserActions('followerIndexForm'));
+
+ await nextTick(); // We need to wait next tick for the mock server response to comes in
+ component.update();
+ });
+
+ test('should display the Follower index form', async () => {
+ expect(exists('ccrFollowerIndexForm')).toBe(true);
+ });
+
+ test('should display errors and disable the save button when clicking "save" without filling the form', () => {
+ expect(exists('followerIndexFormError')).toBe(false);
+ expect(find('ccrFollowerIndexFormSubmitButton').props().disabled).toBe(false);
+
+ clickSaveForm();
+
+ expect(exists('followerIndexFormError')).toBe(true);
+ expect(getFormErrorsMessages()).toEqual([
+ 'Leader index is required.',
+ 'Name is required.'
+ ]);
+ expect(find('ccrFollowerIndexFormSubmitButton').props().disabled).toBe(true);
+ });
+ });
+
+ describe('form validation', () => {
+ beforeEach(async () => {
+ ({ component, form, getUserActions, getFormErrorsMessages, exists, find } = initTestBed(FollowerIndexAdd, undefined, testBedOptions));
+
+ ({ clickSaveForm, toggleAdvancedSettings } = getUserActions('followerIndexForm'));
+
+ await nextTick(); // We need to wait next tick for the mock server response to comes in
+ component.update();
+ });
+
+ describe('remote cluster', () => {
+ // The implementation of the remote cluster "Select" + validation is
+ // done inside the component. The same component that we use in the section.
+ // To avoid copy/pasting the same tests here, we simply make sure that both sections use the
+ test('should use the same component as the section', async () => {
+ const { component: autoFollowPatternAddComponent } = initTestBed(AutoFollowPatternAdd, undefined, testBedOptions);
+ await nextTick();
+ autoFollowPatternAddComponent.update();
+
+ const remoteClusterFormFieldFollowerIndex = component.find(RemoteClustersFormField);
+ const remoteClusterFormFieldAutoFollowPattern = autoFollowPatternAddComponent.find(RemoteClustersFormField);
+
+ expect(remoteClusterFormFieldFollowerIndex.length).toBe(1);
+ expect(remoteClusterFormFieldAutoFollowPattern.length).toBe(1);
+ });
+ });
+
+ describe('leader index', () => {
+ test('should not allow spaces', () => {
+ form.setInputValue('ccrFollowerIndexFormLeaderIndexInput', 'with space');
+ clickSaveForm();
+ expect(getFormErrorsMessages()).toContain('Spaces are not allowed in the leader index.');
+ });
+
+ test('should not allow invalid characters', () => {
+ clickSaveForm(); // Make all errors visible
+
+ const expectInvalidChar = (char) => {
+ form.setInputValue('ccrFollowerIndexFormLeaderIndexInput', `with${char}`);
+ expect(getFormErrorsMessages()).toContain(`Remove the characters ${char} from your leader index.`);
+ };
+
+ return INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.reduce((promise, char) => {
+ return promise.then(() => expectInvalidChar(char));
+ }, Promise.resolve());
+ });
+ });
+
+ describe('follower index', () => {
+ test('should not allow spaces', () => {
+ form.setInputValue('ccrFollowerIndexFormFollowerIndexInput', 'with space');
+ clickSaveForm();
+ expect(getFormErrorsMessages()).toContain('Spaces are not allowed in the name.');
+ });
+
+ test('should not allow a "." (period) as first character', () => {
+ form.setInputValue('ccrFollowerIndexFormFollowerIndexInput', '.withDot');
+ clickSaveForm();
+ expect(getFormErrorsMessages()).toContain(`Name can't begin with a period.`);
+ });
+
+ test('should not allow invalid characters', () => {
+ clickSaveForm(); // Make all errors visible
+
+ const expectInvalidChar = (char) => {
+ form.setInputValue('ccrFollowerIndexFormFollowerIndexInput', `with${char}`);
+ expect(getFormErrorsMessages()).toContain(`Remove the characters ${char} from your name.`);
+ };
+
+ return INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.reduce((promise, char) => {
+ return promise.then(() => expectInvalidChar(char));
+ }, Promise.resolve());
+ });
+
+ describe('ES index name validation', () => {
+ let setGetClusterIndicesResponse;
+ beforeEach(() => {
+ ({ setGetClusterIndicesResponse } = registerHttpRequestMockHelpers(server));
+ });
+
+ test('should make a request to check if the index name is available in ES', async () => {
+ setGetClusterIndicesResponse([]);
+
+ // Keep track of the request count made until this point
+ const totalRequests = server.requests.length;
+
+ form.setInputValue('ccrFollowerIndexFormFollowerIndexInput', 'index-name');
+ await nextTick(550); // we need to wait as there is a debounce of 500ms on the http validation
+
+ expect(server.requests.length).toBe(totalRequests + 1);
+ expect(server.requests[server.requests.length - 1].url).toBe('/api/index_management/indices');
+ });
+
+ test('should display an error if the index already exists', async () => {
+ const indexName = 'index-name';
+ setGetClusterIndicesResponse([{ name: indexName }]);
+
+ form.setInputValue('ccrFollowerIndexFormFollowerIndexInput', indexName);
+ await nextTick(550);
+ component.update();
+
+ expect(getFormErrorsMessages()).toContain('An index with the same name already exists.');
+ });
+ });
+ });
+
+ describe('advanced settings', () => {
+ const advancedSettingsInputFields = {
+ ccrFollowerIndexFormMaxReadRequestOperationCountInput: {
+ default: 5120,
+ type: 'number',
+ },
+ ccrFollowerIndexFormMaxOutstandingReadRequestsInput: {
+ default: 12,
+ type: 'number',
+ },
+ ccrFollowerIndexFormMaxReadRequestSizeInput: {
+ default: '32mb',
+ type: 'string',
+ },
+ ccrFollowerIndexFormMaxWriteRequestOperationCountInput: {
+ default: 5120,
+ type: 'number',
+ },
+ ccrFollowerIndexFormMaxWriteRequestSizeInput: {
+ default: '9223372036854775807b',
+ type: 'string',
+ },
+ ccrFollowerIndexFormMaxOutstandingWriteRequestsInput: {
+ default: 9,
+ type: 'number',
+ },
+ ccrFollowerIndexFormMaxWriteBufferCountInput: {
+ default: 2147483647,
+ type: 'number',
+ },
+ ccrFollowerIndexFormMaxWriteBufferSizeInput: {
+ default: '512mb',
+ type: 'string',
+ },
+ ccrFollowerIndexFormMaxRetryDelayInput: {
+ default: '500ms',
+ type: 'string',
+ },
+ ccrFollowerIndexFormReadPollTimeoutInput: {
+ default: '1m',
+ type: 'string',
+ },
+ };
+
+ test('should have a toggle to activate advanced settings', () => {
+ const expectDoesNotExist = (testSubject) => {
+ try {
+ expect(exists(testSubject)).toBe(false);
+ } catch {
+ throw new Error(`The advanced field "${testSubject}" exists.`);
+ }
+ };
+
+ const expectDoesExist = (testSubject) => {
+ try {
+ expect(exists(testSubject)).toBe(true);
+ } catch {
+ throw new Error(`The advanced field "${testSubject}" does not exist.`);
+ }
+ };
+
+ // Make sure no advanced settings is visible
+ Object.keys(advancedSettingsInputFields).forEach(expectDoesNotExist);
+
+ toggleAdvancedSettings();
+
+ // Make sure no advanced settings is visible
+ Object.keys(advancedSettingsInputFields).forEach(expectDoesExist);
+ });
+
+ test('should set the correct default value for each advanced setting', () => {
+ toggleAdvancedSettings();
+
+ Object.entries(advancedSettingsInputFields).forEach(([testSubject, data]) => {
+ expect(find(testSubject).props().value).toBe(data.default);
+ });
+ });
+
+ test('should set number input field for numeric advanced settings', () => {
+ toggleAdvancedSettings();
+
+ Object.entries(advancedSettingsInputFields).forEach(([testSubject, data]) => {
+ if (data.type === 'number') {
+ expect(find(testSubject).props().type).toBe('number');
+ }
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js
new file mode 100644
index 00000000000000..fc8eabfea18ea0
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js
@@ -0,0 +1,173 @@
+/*
+ * 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 from 'sinon';
+
+import { initTestBed, registerHttpRequestMockHelpers, nextTick, findTestSubject } from './test_helpers';
+import { FollowerIndexAdd } from '../../public/app/sections/follower_index_add';
+import { FollowerIndexEdit } from '../../public/app/sections/follower_index_edit';
+import { FollowerIndexForm } from '../../public/app/components/follower_index_form/follower_index_form';
+import routing from '../../public/app/services/routing';
+
+jest.mock('ui/chrome', () => ({
+ addBasePath: (path) => path || 'api/cross_cluster_replication',
+ breadcrumbs: { set: () => {} },
+}));
+
+jest.mock('ui/index_patterns', () => {
+ const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } =
+ jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants');
+ const { validateIndexPattern, ILLEGAL_CHARACTERS, CONTAINS_SPACES } =
+ jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/validate/validate_index_pattern');
+ return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, validateIndexPattern, ILLEGAL_CHARACTERS, CONTAINS_SPACES };
+});
+
+const FOLLOWER_INDEX_NAME = 'my-follower-index';
+
+const FOLLOWER_INDEX = {
+ name: FOLLOWER_INDEX_NAME,
+ remoteCluster: 'new-york',
+ leaderIndex: 'some-leader-test',
+ status: 'active',
+ maxReadRequestOperationCount: 7845,
+ maxOutstandingReadRequests: 16,
+ maxReadRequestSize: '64mb',
+ maxWriteRequestOperationCount: 2456,
+ maxWriteRequestSize: '1048b',
+ maxOutstandingWriteRequests: 69,
+ maxWriteBufferCount: 123456,
+ maxWriteBufferSize: '256mb',
+ maxRetryDelay: '225ms',
+ readPollTimeout: '2m'
+};
+
+const testBedOptions = {
+ memoryRouter: {
+ onRouter: (router) => routing.reactRouter = router,
+ // The auto-follow pattern id to fetch is read from the router ":id" param
+ // so we first set it in our initial entries
+ initialEntries: [`/${FOLLOWER_INDEX_NAME}`],
+ // and then we declarae the :id param on the component route path
+ componentRoutePath: '/:id'
+ }
+};
+
+describe('Edit Auto-follow pattern', () => {
+ let server;
+ let find;
+ let component;
+ let getUserActions;
+ let getFormErrorsMessages;
+ let clickSaveForm;
+ let setLoadRemoteClustersResponse;
+ let setGetFollowerIndexResponse;
+
+ beforeEach(() => {
+ server = sinon.fakeServer.create();
+ server.respondImmediately = true;
+
+ // Register helpers to mock Http Requests
+ ({
+ setLoadRemoteClustersResponse,
+ setGetFollowerIndexResponse
+ } = registerHttpRequestMockHelpers(server));
+
+ // Set "default" mock responses by not providing any arguments
+ setLoadRemoteClustersResponse();
+
+ // Mock all HTTP Requests that have not been handled previously
+ server.respondWith([200, {}, '']);
+ });
+
+ describe('on component mount', () => {
+ const remoteClusters = [
+ { name: 'new-york', seeds: ['localhost:123'], isConnected: true },
+ ];
+
+ beforeEach(async () => {
+ setLoadRemoteClustersResponse(remoteClusters);
+ setGetFollowerIndexResponse(FOLLOWER_INDEX);
+ ({ component, find } = initTestBed(FollowerIndexEdit, undefined, testBedOptions));
+
+ await nextTick();
+ component.update();
+ });
+
+ /**
+ * As the "edit" follower index component uses the same form underneath that
+ * the "create" follower index, we won't test it again but simply make sure that
+ * the form component is indeed shared between the 2 app sections.
+ */
+ test('should use the same Form component as the " " component', async () => {
+ const { component: addFollowerIndexComponent } = initTestBed(FollowerIndexAdd, undefined, testBedOptions);
+
+ await nextTick();
+ addFollowerIndexComponent.update();
+
+ const formEdit = component.find(FollowerIndexForm);
+ const formAdd = addFollowerIndexComponent.find(FollowerIndexForm);
+
+ expect(formEdit.length).toBe(1);
+ expect(formAdd.length).toBe(1);
+ });
+
+ test('should populate the form fields with the values from the follower index loaded', () => {
+ const inputToPropMap = {
+ ccrRemoteClusterInput: 'remoteCluster',
+ ccrFollowerIndexFormLeaderIndexInput: 'leaderIndex',
+ ccrFollowerIndexFormFollowerIndexInput: 'name',
+ ccrFollowerIndexFormMaxReadRequestOperationCountInput: 'maxReadRequestOperationCount',
+ ccrFollowerIndexFormMaxOutstandingReadRequestsInput: 'maxOutstandingReadRequests',
+ ccrFollowerIndexFormMaxReadRequestSizeInput: 'maxReadRequestSize',
+ ccrFollowerIndexFormMaxWriteRequestOperationCountInput: 'maxWriteRequestOperationCount',
+ ccrFollowerIndexFormMaxWriteRequestSizeInput: 'maxWriteRequestSize',
+ ccrFollowerIndexFormMaxOutstandingWriteRequestsInput: 'maxOutstandingWriteRequests',
+ ccrFollowerIndexFormMaxWriteBufferCountInput: 'maxWriteBufferCount',
+ ccrFollowerIndexFormMaxWriteBufferSizeInput: 'maxWriteBufferSize',
+ ccrFollowerIndexFormMaxRetryDelayInput: 'maxRetryDelay',
+ ccrFollowerIndexFormReadPollTimeoutInput: 'readPollTimeout',
+ };
+
+ Object.entries(inputToPropMap).forEach(([input, prop]) => {
+ const expected = FOLLOWER_INDEX[prop];
+ const { value } = find(input).props();
+ try {
+ expect(value).toBe(expected);
+ } catch {
+ throw new Error(`Input "${input}" does not equal "${expected}". (Value received: "${value}")`);
+ }
+ });
+ });
+ });
+
+ describe('when the remote cluster is disconnected', () => {
+ beforeEach(async () => {
+ setLoadRemoteClustersResponse([{ name: 'new-york', seeds: ['localhost:123'], isConnected: false }]);
+ setGetFollowerIndexResponse(FOLLOWER_INDEX);
+ ({ component, find, getUserActions, getFormErrorsMessages } = initTestBed(FollowerIndexEdit, undefined, testBedOptions));
+ ({ clickSaveForm } = getUserActions('followerIndexForm'));
+
+ await nextTick();
+ component.update();
+ });
+
+ test('should display an error and have a button to edit the remote cluster', () => {
+ const error = find('remoteClusterFieldCallOutError');
+
+ expect(error.length).toBe(1);
+ expect(error.find('.euiCallOutHeader__title').text())
+ .toBe(`Can't edit follower index because remote cluster '${FOLLOWER_INDEX.remoteCluster}' is not connected`);
+ expect(findTestSubject(error, 'ccrRemoteClusterEditButton').length).toBe(1);
+ });
+
+ test('should prevent saving the form and display an error message for the required remote cluster', () => {
+ clickSaveForm();
+
+ expect(getFormErrorsMessages()).toEqual(['A connected remote cluster is required.']);
+ expect(find('ccrFollowerIndexFormSubmitButton').props().disabled).toBe(true);
+ });
+ });
+});
diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js
index 4b48b37a0c1936..645791da10dba2 100644
--- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js
+++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js
@@ -16,7 +16,8 @@ jest.mock('ui/chrome', () => ({
}));
jest.mock('ui/index_patterns', () => {
- const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } = require.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants'); // eslint-disable-line max-len
+ const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } =
+ require.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants');
return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE };
});
diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js
index b8056e26253424..f99c138b4ea9be 100644
--- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js
+++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js
@@ -17,7 +17,8 @@ jest.mock('ui/chrome', () => ({
}));
jest.mock('ui/index_patterns', () => {
- const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } = require.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants'); // eslint-disable-line max-len
+ const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } =
+ require.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants');
return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE };
});
diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/test_helpers.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/test_helpers.js
index ba0cdb0a671692..19820811874e01 100644
--- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/test_helpers.js
+++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/test_helpers.js
@@ -26,9 +26,9 @@ const $q = { defer: () => ({ resolve() {} }) };
// axios has a $http like interface so using it to simulate $http
setHttpClient(axios.create(), $q);
-const initUserActions = ({ getMetadataFromEuiTable, find }) => (section) => {
+const initUserActions = ({ getMetadataFromEuiTable, find, form }) => (section) => {
const userActions = {
- // Follower indices user actions
+ // Follower indices LIST
followerIndicesList() {
const { rows } = getMetadataFromEuiTable('ccrFollowerIndexListTable');
@@ -70,7 +70,7 @@ const initUserActions = ({ getMetadataFromEuiTable, find }) => (section) => {
clickFollowerIndexAt,
};
},
- // Auto-follow patterns user actions
+ // Auto-follow patterns LIST
autoFollowPatternList() {
const { rows } = getMetadataFromEuiTable('ccrAutoFollowPatternListTable');
@@ -119,6 +119,31 @@ const initUserActions = ({ getMetadataFromEuiTable, find }) => (section) => {
clickRowActionButtonAt,
clickAutoFollowPatternAt
};
+ },
+ // Auto-follow pattern FORM
+ autoFollowPatternForm() {
+ const clickSaveForm = () => {
+ find('ccrAutoFollowPatternFormSubmitButton').simulate('click');
+ };
+
+ return {
+ clickSaveForm,
+ };
+ },
+ // Follower index FORM
+ followerIndexForm() {
+ const clickSaveForm = () => {
+ find('ccrFollowerIndexFormSubmitButton').simulate('click');
+ };
+
+ const toggleAdvancedSettings = () => {
+ form.selectCheckBox('ccrFollowerIndexFormCustomAdvancedSettingsToggle');
+ };
+
+ return {
+ clickSaveForm,
+ toggleAdvancedSettings,
+ };
}
};
@@ -131,9 +156,25 @@ export const initTestBed = (component, props = {}, options) => {
const testBed = registerTestBed(component, {}, ccrStore)(props, options);
const getUserActions = initUserActions(testBed);
+ // Cutsom Form helpers
+ const setIndexPatternValue = (value) => {
+ const comboBox = testBed.find('ccrAutoFollowPatternFormIndexPatternInput');
+ const indexPatternsInput = findTestSubject(comboBox, 'comboBoxSearchInput');
+ testBed.form.setInputValue(indexPatternsInput, value);
+
+ // We need to press the ENTER key in order for the EuiComboBox to register
+ // the value. (keyCode 13 === ENTER)
+ comboBox.simulate('keydown', { keyCode: 13 });
+ testBed.component.update();
+ };
+
return {
...testBed,
getUserActions,
+ form: {
+ ...testBed.form,
+ setIndexPatternValue,
+ }
};
};
@@ -185,10 +226,47 @@ export const registerHttpRequestMockHelpers = server => {
);
};
+ const setLoadRemoteClustersResponse = (response = [], error) => {
+ if (error) {
+ server.respondWith('GET', '/api/remote_clusters',
+ [error.status || 400, { 'Content-Type': 'application/json' }, JSON.stringify(error.body)]
+ );
+ } else {
+ server.respondWith('GET', '/api/remote_clusters',
+ [200, { 'Content-Type': 'application/json' }, JSON.stringify(response)]
+ );
+ }
+ };
+
+ const setGetAutoFollowPatternResponse = (response) => {
+ const defaultResponse = {};
+
+ server.respondWith('GET', /api\/cross_cluster_replication\/auto_follow_patterns\/.+/,
+ mockResponse(defaultResponse, response)
+ );
+ };
+
+ const setGetClusterIndicesResponse = (response = []) => {
+ server.respondWith('GET', '/api/index_management/indices',
+ [200, { 'Content-Type': 'application/json' }, JSON.stringify(response)]);
+ };
+
+ const setGetFollowerIndexResponse = (response) => {
+ const defaultResponse = {};
+
+ server.respondWith('GET', /api\/cross_cluster_replication\/follower_indices\/.+/,
+ mockResponse(defaultResponse, response)
+ );
+ };
+
return {
setLoadFollowerIndicesResponse,
setLoadAutoFollowPatternsResponse,
setDeleteAutoFollowPatternResponse,
setAutoFollowStatsResponse,
+ setLoadRemoteClustersResponse,
+ setGetAutoFollowPatternResponse,
+ setGetClusterIndicesResponse,
+ setGetFollowerIndexResponse,
};
};
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js
index 7068bf33d3318d..a69527cc82ee57 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js
@@ -148,13 +148,10 @@ export class AutoFollowPatternForm extends PureComponent {
};
onLeaderIndexPatternInputChange = (leaderIndexPattern) => {
- if (!leaderIndexPattern || !leaderIndexPattern.trim()) {
- return;
- }
-
+ const isEmpty = !leaderIndexPattern || !leaderIndexPattern.trim();
const { autoFollowPattern: { leaderIndexPatterns } } = this.state;
- if (leaderIndexPatterns.includes(leaderIndexPattern)) {
+ if (!isEmpty && leaderIndexPatterns.includes(leaderIndexPattern)) {
const errorMsg = i18n.translate(
'xpack.crossClusterReplication.autoFollowPatternForm.leaderIndexPatternError.duplicateMessage',
{
@@ -172,7 +169,12 @@ export class AutoFollowPatternForm extends PureComponent {
this.setState(({ fieldsErrors }) => updateFormErrors(errors, fieldsErrors));
} else {
this.setState(({ fieldsErrors, autoFollowPattern: { leaderIndexPatterns } }) => {
- const errors = validateAutoFollowPattern({ leaderIndexPatterns });
+ const errors = Boolean(leaderIndexPatterns.length)
+ // Validate existing patterns, so we can surface an error if this required input is missing.
+ ? validateAutoFollowPattern({ leaderIndexPatterns })
+ // Validate the input as the user types so they have immediate feedback about errors.
+ : validateAutoFollowPattern({ leaderIndexPatterns: [leaderIndexPattern] });
+
return updateFormErrors(errors, fieldsErrors);
});
}
@@ -223,7 +225,7 @@ export class AutoFollowPatternForm extends PureComponent {
return (
-
+
);
@@ -372,7 +374,7 @@ export class AutoFollowPatternForm extends PureComponent {
* Leader index pattern(s)
*/
const renderLeaderIndexPatterns = () => {
- const hasError = !!fieldsErrors.leaderIndexPatterns;
+ const hasError = !!(fieldsErrors.leaderIndexPatterns && fieldsErrors.leaderIndexPatterns.message);
const isInvalid = hasError && (fieldsErrors.leaderIndexPatterns.alwaysVisible || areErrorsVisible);
const formattedLeaderIndexPatterns = leaderIndexPatterns.map(pattern => ({ label: pattern }));
@@ -576,6 +578,7 @@ export class AutoFollowPatternForm extends PureComponent {
)}
color="danger"
iconType="cross"
+ data-test-subj="autoFollowPatternFormError"
/>
@@ -643,7 +646,7 @@ export class AutoFollowPatternForm extends PureComponent {
return (
-
+
{renderAutoFollowPatternName()}
{renderRemoteClusterField()}
{renderLeaderIndexPatterns()}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js
index c76c13c44d51fa..2c632462419df3 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js
@@ -29,6 +29,7 @@ export const AutoFollowPatternIndicesPreview = ({ prefix, suffix, leaderIndexPat
(
href={autoFollowPatternUrl}
target="_blank"
iconType="help"
+ data-test-subj="autoFollowPatternDocsButton"
>
{
this.onFieldsChange({ name });
+ const error = indexNameValidator(name);
+ if (error) {
+ // If there is a client side error
+ // there is no need to validate the name
+ return;
+ }
+
if (!name || !name.trim()) {
this.setState({
isValidatingIndexName: false,
@@ -613,6 +620,7 @@ export class FollowerIndexForm extends PureComponent {
)}
color="danger"
iconType="cross"
+ data-test-subj="followerIndexFormError"
/>
@@ -681,7 +689,7 @@ export class FollowerIndexForm extends PureComponent {
return (
-
+
{renderRemoteClusterField()}
{renderLeaderIndex()}
{renderFollowerIndexName()}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js
index a1ec4a44e218a1..c04081a88a5e44 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js
@@ -38,6 +38,7 @@ export const FollowerIndexPageTitle = ({ title }) => (
href={followerIndexUrl}
target="_blank"
iconType="help"
+ data-test-subj="followerIndexDocsButton"
>
{ areErrorsVisible && Boolean(errorMessage) ? this.renderValidRemoteClusterRequired() : null }
{ errorMessage }
@@ -137,23 +138,21 @@ export class RemoteClustersFormField extends PureComponent {
{ areErrorsVisible && Boolean(errorMessage) ? this.renderValidRemoteClusterRequired() : null }
{ errorMessage }
-
-
- {/* Break out of EuiFormRow's flexbox layout */}
-
-
-
-
-
+
+ {/* Break out of EuiFormRow's flexbox layout */}
+
+
+
+
);
};
@@ -170,6 +169,7 @@ export class RemoteClustersFormField extends PureComponent {
title={title}
color="danger"
iconType="cross"
+ data-test-subj="remoteClusterFieldNoClusterFoundError"
>
{ this.errorMessages.noClusterFound() }
@@ -207,6 +207,7 @@ export class RemoteClustersFormField extends PureComponent {
title={title}
color={fatal ? 'danger' : 'warning'}
iconType="cross"
+ data-test-subj="remoteClusterFieldCallOutError"
>
{ description }
@@ -318,9 +319,7 @@ export class RemoteClustersFormField extends PureComponent {
isInvalid={isInvalid}
fullWidth
>
-
- {field}
-
+ {field}
);
}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js
index 01d4bf11e034c5..f7b3eb149c8250 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js
@@ -27,10 +27,11 @@ export class RemoteClustersProvider extends PureComponent {
})
);
loadRemoteClusters()
+ .then(sortClusterByName)
.then((remoteClusters) => {
this.setState({
isLoading: false,
- remoteClusters: sortClusterByName(remoteClusters)
+ remoteClusters
});
})
.catch((error) => {
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js
index 4988449802b534..b6c2e0544cd94f 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js
@@ -10,7 +10,8 @@ import {
EuiSpacer,
} from '@elastic/eui';
-export function SectionError({ title, error }) {
+export function SectionError(props) {
+ const { title, error, ...rest } = props;
const data = error.data ? error.data : error;
const {
error: errorString,
@@ -23,6 +24,7 @@ export function SectionError({ title, error }) {
title={title}
color="danger"
iconType="alert"
+ {...rest}
>
{message || errorString}
{ cause && (
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
index 4fa0afd2fe579a..65c23b27f6cb23 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
@@ -56,7 +56,7 @@ export class AutoFollowPatternAdd extends PureComponent {
{({ isLoading, error, remoteClusters }) => {
if (isLoading) {
return (
-
+
{
if (isLoading) {
return (
-
+
(
);
export const loadRemoteClusters = () => (
- httpClient.get(`${apiPrefixRemoteClusters}`).then(extractData)
+ httpClient.get(apiPrefixRemoteClusters).then(extractData)
);
export const createAutoFollowPattern = (autoFollowPattern) => (
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js
index 94a0d34fd4e4b6..87bbceedb201be 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js
@@ -53,29 +53,53 @@ export const validateName = (name = '') => {
};
export const validateLeaderIndexPattern = (indexPattern) => {
- const errors = getIndexPatternErrors(indexPattern);
-
- if (errors[ILLEGAL_CHARACTERS]) {
- return (
- {errors[ILLEGAL_CHARACTERS].join(' ')},
- characterListLength: errors[ILLEGAL_CHARACTERS].length,
- }}
- />
- );
+ values={{
+ characterList: {errors[ILLEGAL_CHARACTERS].join(' ')} ,
+ characterListLength: errors[ILLEGAL_CHARACTERS].length,
+ }}
+ />
+ });
+ }
+
+ if (errors[CONTAINS_SPACES]) {
+ return ({
+ message:
+ });
+ }
}
- if (errors[CONTAINS_SPACES]) {
- return (
-
- );
+ if (!indexPattern || !indexPattern.trim()) {
+ return {
+ message: i18n.translate('xpack.crossClusterReplication.autoFollowPattern.leaderIndexPatternValidation.isEmpty', {
+ defaultMessage: 'At least one leader index pattern is required.',
+ })
+ };
+ }
+
+ return null;
+};
+
+export const validateLeaderIndexPatterns = (indexPatterns) => {
+ // We only need to check if a value has been provided, because validation for this field
+ // has already been executed as the user has entered input into it.
+ if (!indexPatterns.length) {
+ return {
+ message: i18n.translate('xpack.crossClusterReplication.autoFollowPattern.leaderIndexPatternValidation.isEmpty', {
+ defaultMessage: 'At least one leader index pattern is required.',
+ })
+ };
}
return null;
@@ -174,13 +198,7 @@ export const validateAutoFollowPattern = (autoFollowPattern = {}) => {
break;
case 'leaderIndexPatterns':
- if (!fieldValue.length) {
- error = {
- message: i18n.translate('xpack.crossClusterReplication.autoFollowPattern.leaderIndexPatternValidation.isEmpty', {
- defaultMessage: 'At least one leader index pattern is required.',
- })
- };
- }
+ error = validateLeaderIndexPatterns(fieldValue);
break;
case 'followIndexPatternPrefix':
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js
index 31e55bdd8a5911..d859fe0191303c 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js
@@ -12,6 +12,12 @@ const isEmpty = value => {
return !value || !value.trim().length;
};
+const hasSpaces = (value) => (
+ typeof value === 'string'
+ ? value.includes(' ')
+ : false
+);
+
const beginsWithPeriod = value => {
return value[0] === '.';
};
@@ -57,6 +63,15 @@ export const indexNameValidator = (value) => {
)];
}
+ if(hasSpaces(value)) {
+ return [(
+
+ )];
+ }
+
return undefined;
};
@@ -82,5 +97,14 @@ export const leaderIndexValidator = (value) => {
)];
}
+ if(hasSpaces(value)) {
+ return [(
+
+ )];
+ }
+
return undefined;
};
diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx
index a7d3b81d66d4b9..c20c423b700680 100644
--- a/x-pack/plugins/infra/public/apps/start_app.tsx
+++ b/x-pack/plugins/infra/public/apps/start_app.tsx
@@ -20,6 +20,7 @@ import { InfraFrontendLibs } from '../lib/lib';
import { PageRouter } from '../routes';
import { createStore } from '../store';
import { ApolloClientContext } from '../utils/apollo_context';
+import { useKibanaUiSetting } from '../utils/use_kibana_ui_setting';
export async function startApp(libs: InfraFrontendLibs) {
const history = createHashHistory();
@@ -30,21 +31,27 @@ export async function startApp(libs: InfraFrontendLibs) {
observableApi: libs$.pipe(pluck('observableApi')),
});
- libs.framework.render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ const InfraPluginRoot: React.FunctionComponent = () => {
+ const [darkMode] = useKibanaUiSetting('theme:darkMode');
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ libs.framework.render( );
}
diff --git a/x-pack/plugins/infra/public/components/formatted_time.tsx b/x-pack/plugins/infra/public/components/formatted_time.tsx
index 0a5b32ec4cdf6a..1afe4ed39485c7 100644
--- a/x-pack/plugins/infra/public/components/formatted_time.tsx
+++ b/x-pack/plugins/infra/public/components/formatted_time.tsx
@@ -5,8 +5,9 @@
*/
import moment from 'moment';
-import React from 'react';
-import { WithKibanaChrome } from '../containers/with_kibana_chrome';
+import React, { useMemo } from 'react';
+
+import { useKibanaUiSetting } from '../utils/use_kibana_ui_setting';
interface FormattedTimeProps {
time: number; // Unix time (milliseconds)
@@ -21,8 +22,17 @@ const getFormattedTime = (
return userFormat ? moment(time).format(userFormat) : moment(time).format(fallbackFormat);
};
-export const FormattedTime: React.SFC = ({ time, fallbackFormat }) => (
-
- {({ dateFormat }) => {getFormattedTime(time, dateFormat, fallbackFormat)} }
-
-);
+export const FormattedTime: React.FunctionComponent = ({
+ time,
+ fallbackFormat,
+}) => {
+ const [dateFormat] = useKibanaUiSetting('dateFormat');
+ const formattedTime = useMemo(() => getFormattedTime(time, dateFormat, fallbackFormat), [
+ getFormattedTime,
+ time,
+ dateFormat,
+ fallbackFormat,
+ ]);
+
+ return {formattedTime} ;
+};
diff --git a/x-pack/plugins/infra/public/components/metrics/index.tsx b/x-pack/plugins/infra/public/components/metrics/index.tsx
index c27425936c0d45..4727f7836918ba 100644
--- a/x-pack/plugins/infra/public/components/metrics/index.tsx
+++ b/x-pack/plugins/infra/public/components/metrics/index.tsx
@@ -8,9 +8,8 @@ import { EuiPageContentBody, EuiTitle } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
-import { InfraMetricData } from '../../graphql/types';
+import { InfraMetricData, InfraTimerangeInput } from '../../graphql/types';
import { InfraMetricLayout, InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
-import { metricTimeActions } from '../../store';
import { NoData } from '../empty_states';
import { InfraLoadingPanel } from '../loading';
import { Section } from './section';
@@ -22,7 +21,7 @@ interface Props {
refetch: () => void;
nodeId: string;
label: string;
- onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
+ onChangeRangeTime?: (time: InfraTimerangeInput) => void;
isLiveStreaming?: boolean;
stopLiveStreaming?: () => void;
intl: InjectedIntl;
diff --git a/x-pack/plugins/infra/public/components/metrics/section.tsx b/x-pack/plugins/infra/public/components/metrics/section.tsx
index 8f4a44fa935a74..a569d4796b4de1 100644
--- a/x-pack/plugins/infra/public/components/metrics/section.tsx
+++ b/x-pack/plugins/infra/public/components/metrics/section.tsx
@@ -5,15 +5,14 @@
*/
import React from 'react';
-import { InfraMetricData } from '../../graphql/types';
+import { InfraMetricData, InfraTimerangeInput } from '../../graphql/types';
import { InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
-import { metricTimeActions } from '../../store';
import { sections } from './sections';
interface Props {
section: InfraMetricLayoutSection;
metrics: InfraMetricData[];
- onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
+ onChangeRangeTime?: (time: InfraTimerangeInput) => void;
crosshairValue?: number;
onCrosshairUpdate?: (crosshairValue: number) => void;
isLiveStreaming?: boolean;
diff --git a/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx b/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx
index 7db5d74336a14e..bab6684960c6b5 100644
--- a/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx
+++ b/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx
@@ -21,13 +21,12 @@ import Color from 'color';
import { get } from 'lodash';
import moment from 'moment';
import React, { ReactText } from 'react';
-import { InfraDataSeries, InfraMetricData } from '../../../graphql/types';
+import { InfraDataSeries, InfraMetricData, InfraTimerangeInput } from '../../../graphql/types';
import { InfraFormatter, InfraFormatterType } from '../../../lib/lib';
import {
InfraMetricLayoutSection,
InfraMetricLayoutVisualizationType,
} from '../../../pages/metrics/layouts/types';
-import { metricTimeActions } from '../../../store';
import { createFormatter } from '../../../utils/formatters';
const MARGIN_LEFT = 60;
@@ -40,7 +39,7 @@ const chartComponentsByType = {
interface Props {
section: InfraMetricLayoutSection;
metric: InfraMetricData;
- onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
+ onChangeRangeTime?: (time: InfraTimerangeInput) => void;
crosshairValue?: number;
onCrosshairUpdate?: (crosshairValue: number) => void;
isLiveStreaming?: boolean;
@@ -176,6 +175,7 @@ export const ChartSection = injectI18n(
=1m',
+ });
}
};
}
diff --git a/x-pack/plugins/infra/public/components/metrics/time_controls.tsx b/x-pack/plugins/infra/public/components/metrics/time_controls.tsx
index 2b1d405c2d5ba6..1d5a5c80954e3d 100644
--- a/x-pack/plugins/infra/public/components/metrics/time_controls.tsx
+++ b/x-pack/plugins/infra/public/components/metrics/time_controls.tsx
@@ -4,226 +4,65 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import moment, { Moment } from 'moment';
+import dateMath from '@elastic/datemath';
+import { EuiSuperDatePicker, OnRefreshChangeProps, OnTimeChangeProps } from '@elastic/eui';
+import moment from 'moment';
import React from 'react';
-
import euiStyled from '../../../../../common/eui_styled_components';
-import { metricTimeActions } from '../../store';
-import { RangeDatePicker, RecentlyUsed } from '../range_date_picker';
+import { InfraTimerangeInput } from '../../graphql/types';
+
+const EuiSuperDatePickerAbsoluteFormat = 'YYYY-MM-DDTHH:mm:ss.sssZ';
interface MetricsTimeControlsProps {
- currentTimeRange: metricTimeActions.MetricRangeTimeState;
+ currentTimeRange: InfraTimerangeInput;
isLiveStreaming?: boolean;
- onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
- startLiveStreaming?: () => void;
- stopLiveStreaming?: () => void;
-}
-
-interface MetricsTimeControlsState {
- showGoButton: boolean;
- to: moment.Moment | undefined;
- from: moment.Moment | undefined;
- recentlyUsed: RecentlyUsed[];
+ refreshInterval?: number | null;
+ onChangeTimeRange: (time: InfraTimerangeInput) => void;
+ setRefreshInterval: (refreshInterval: number) => void;
+ setAutoReload: (isAutoReloading: boolean) => void;
}
-export class MetricsTimeControls extends React.Component<
- MetricsTimeControlsProps,
- MetricsTimeControlsState
-> {
- public dateRangeRef: React.RefObject = React.createRef();
- public readonly state = {
- showGoButton: false,
- to: moment().millisecond(this.props.currentTimeRange.to),
- from: moment().millisecond(this.props.currentTimeRange.from),
- recentlyUsed: [],
- };
+export class MetricsTimeControls extends React.Component {
public render() {
- const { currentTimeRange, isLiveStreaming } = this.props;
- const { showGoButton, to, from, recentlyUsed } = this.state;
-
- const liveStreamingButton = (
-
-
- {isLiveStreaming ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
-
- );
-
- const goColor = from && to && from > to ? 'danger' : 'primary';
- const appendButton = showGoButton ? (
-
-
-
-
-
-
-
-
-
-
-
-
- ) : (
- liveStreamingButton
- );
+ const { currentTimeRange, isLiveStreaming, refreshInterval } = this.props;
return (
-
- {appendButton}
);
}
- private handleChangeDate = (
- from: Moment | undefined,
- to: Moment | undefined,
- search: boolean
- ) => {
- const { onChangeRangeTime } = this.props;
- const duration = moment.duration(from && to ? from.diff(to) : 0);
- const milliseconds = duration.asMilliseconds();
- if (to && from && onChangeRangeTime && search && to > from) {
- this.setState({
- showGoButton: false,
- to,
- from,
- });
- onChangeRangeTime({
- to: to && to.valueOf(),
- from: from && from.valueOf(),
- } as metricTimeActions.MetricRangeTimeState);
- } else if (milliseconds !== 0) {
- this.setState({
- showGoButton: true,
- to,
- from,
- });
- }
- };
+ private handleTimeChange = ({ start, end }: OnTimeChangeProps) => {
+ const parsedStart = dateMath.parse(start);
+ const parsedEnd = dateMath.parse(end);
- private searchRangeTime = () => {
- const { onChangeRangeTime } = this.props;
- const { to, from, recentlyUsed } = this.state;
- if (to && from && onChangeRangeTime && to > from) {
- this.setState({
- ...this.state,
- showGoButton: false,
- recentlyUsed: [
- ...recentlyUsed,
- ...[
- {
- type: 'date-range',
- text: [from.format('L LTS'), to.format('L LTS')],
- },
- ],
- ],
+ if (parsedStart && parsedEnd) {
+ this.props.onChangeTimeRange({
+ from: parsedStart.valueOf(),
+ to: parsedEnd.valueOf(),
+ interval: '>=1m',
});
- onChangeRangeTime({
- to: to.valueOf(),
- from: from.valueOf(),
- } as metricTimeActions.MetricRangeTimeState);
}
};
- private startLiveStreaming = () => {
- const { startLiveStreaming } = this.props;
-
- if (startLiveStreaming) {
- startLiveStreaming();
- }
- };
-
- private stopLiveStreaming = () => {
- const { stopLiveStreaming } = this.props;
-
- if (stopLiveStreaming) {
- stopLiveStreaming();
- }
- };
-
- private cancelSearch = () => {
- const { onChangeRangeTime } = this.props;
- const to = moment(this.props.currentTimeRange.to);
- const from = moment(this.props.currentTimeRange.from);
-
- this.setState({
- ...this.state,
- showGoButton: false,
- to,
- from,
- });
- this.dateRangeRef.current.resetRangeDate(from, to);
- if (onChangeRangeTime) {
- onChangeRangeTime({
- to: to && to.valueOf(),
- from: from && from.valueOf(),
- } as metricTimeActions.MetricRangeTimeState);
- }
- };
-
- private resetSearch = () => {
- const { onChangeRangeTime } = this.props;
- const to = moment();
- const from = moment().subtract(1, 'hour');
- if (onChangeRangeTime) {
- onChangeRangeTime({
- to: to.valueOf(),
- from: from.valueOf(),
- } as metricTimeActions.MetricRangeTimeState);
+ private handleRefreshChange = ({ isPaused, refreshInterval }: OnRefreshChangeProps) => {
+ if (isPaused) {
+ this.props.setAutoReload(false);
+ } else {
+ this.props.setRefreshInterval(refreshInterval);
+ this.props.setAutoReload(true);
}
};
}
const MetricsTimeControlsContainer = euiStyled.div`
- display: flex;
- justify-content: right;
- flex-flow: row wrap;
- & > div:first-child {
- margin-right: 0.5rem;
- }
+ max-width: 750px;
`;
diff --git a/x-pack/plugins/infra/public/components/range_date_picker/index.tsx b/x-pack/plugins/infra/public/components/range_date_picker/index.tsx
deleted file mode 100644
index be25a8cc762ce2..00000000000000
--- a/x-pack/plugins/infra/public/components/range_date_picker/index.tsx
+++ /dev/null
@@ -1,601 +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 { i18n } from '@kbn/i18n';
-import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
-import { find, get } from 'lodash';
-import moment from 'moment';
-import React, { Fragment } from 'react';
-
-import {
- EuiButton,
- EuiButtonEmpty,
- EuiDatePicker,
- EuiDatePickerRange,
- EuiFieldNumber,
- EuiFlexGrid,
- EuiFlexGroup,
- EuiFlexItem,
- EuiFormControlLayout,
- EuiFormRow,
- EuiHorizontalRule,
- EuiIcon,
- EuiLink,
- EuiPopover,
- EuiSelect,
- EuiSpacer,
- EuiText,
- EuiTitle,
-} from '@elastic/eui';
-
-enum DatePickerDateOptions {
- today = 'today',
- yesterday = 'yesterday',
- thisWeek = 'this_week',
- weekToDate = 'week_to_date',
- thisMonth = 'this_month',
- monthToDate = 'month_to_date',
- thisYear = 'this_year',
- yearToDate = 'year_to_date',
-}
-
-const commonDates: Array<{ id: string; label: any }> = [
- {
- id: DatePickerDateOptions.today,
- label: i18n.translate('xpack.infra.rangeDatePicker.todayText', {
- defaultMessage: 'Today',
- }),
- },
- {
- id: DatePickerDateOptions.yesterday,
- label: i18n.translate('xpack.infra.rangeDatePicker.yesterdayText', {
- defaultMessage: 'Yesterday',
- }),
- },
- {
- id: DatePickerDateOptions.thisWeek,
- label: i18n.translate('xpack.infra.rangeDatePicker.thisWeekText', {
- defaultMessage: 'This week',
- }),
- },
- {
- id: DatePickerDateOptions.weekToDate,
- label: i18n.translate('xpack.infra.rangeDatePicker.weekToDateText', {
- defaultMessage: 'Week to date',
- }),
- },
- {
- id: DatePickerDateOptions.thisMonth,
- label: i18n.translate('xpack.infra.rangeDatePicker.thisMonthText', {
- defaultMessage: 'This month',
- }),
- },
- {
- id: DatePickerDateOptions.monthToDate,
- label: i18n.translate('xpack.infra.rangeDatePicker.monthToDateText', {
- defaultMessage: 'Month to date',
- }),
- },
- {
- id: DatePickerDateOptions.thisYear,
- label: i18n.translate('xpack.infra.rangeDatePicker.thisYearText', {
- defaultMessage: 'This year',
- }),
- },
- {
- id: DatePickerDateOptions.yearToDate,
- label: i18n.translate('xpack.infra.rangeDatePicker.yearToDateText', {
- defaultMessage: 'Year to date',
- }),
- },
-];
-
-const singleLastOptions: Array<{ value: string; text: any }> = [
- {
- value: 'seconds',
- text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.secondLabel', {
- defaultMessage: 'second',
- }),
- },
- {
- value: 'minutes',
- text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.minuteLabel', {
- defaultMessage: 'minute',
- }),
- },
- {
- value: 'hours',
- text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.hourLabel', {
- defaultMessage: 'hour',
- }),
- },
- {
- value: 'days',
- text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.dayLabel', {
- defaultMessage: 'day',
- }),
- },
- {
- value: 'weeks',
- text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.weekLabel', {
- defaultMessage: 'week',
- }),
- },
- {
- value: 'months',
- text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.monthLabel', {
- defaultMessage: 'month',
- }),
- },
- {
- value: 'years',
- text: i18n.translate('xpack.infra.rangeDatePicker.singleUnitOptions.yearLabel', {
- defaultMessage: 'year',
- }),
- },
-];
-
-const pluralLastOptions: Array<{ value: string; text: any }> = [
- {
- value: 'seconds',
- text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.secondsLabel', {
- defaultMessage: 'seconds',
- }),
- },
- {
- value: 'minutes',
- text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.minutesLabel', {
- defaultMessage: 'minutes',
- }),
- },
- {
- value: 'hours',
- text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.hoursLabel', {
- defaultMessage: 'hours',
- }),
- },
- {
- value: 'days',
- text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.daysLabel', {
- defaultMessage: 'days',
- }),
- },
- {
- value: 'weeks',
- text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.weeksLabel', {
- defaultMessage: 'weeks',
- }),
- },
- {
- value: 'months',
- text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.monthsLabel', {
- defaultMessage: 'months',
- }),
- },
- {
- value: 'years',
- text: i18n.translate('xpack.infra.rangeDatePicker.pluralUnitOptions.yearsLabel', {
- defaultMessage: 'years',
- }),
- },
-];
-
-interface RangeDatePickerProps {
- startDate: moment.Moment | undefined;
- endDate: moment.Moment | undefined;
- onChangeRangeTime: (
- from: moment.Moment | undefined,
- to: moment.Moment | undefined,
- search: boolean
- ) => void;
- recentlyUsed: RecentlyUsed[];
- disabled?: boolean;
- isLoading?: boolean;
- ref?: React.RefObject;
- intl: InjectedIntl;
-}
-
-export interface RecentlyUsed {
- type: string;
- text: string | string[];
-}
-
-interface RangeDatePickerState {
- startDate: moment.Moment | undefined;
- endDate: moment.Moment | undefined;
- isPopoverOpen: boolean;
- recentlyUsed: RecentlyUsed[];
- quickSelectTime: number;
- quickSelectUnit: string;
-}
-
-export const RangeDatePicker = injectI18n(
- class extends React.PureComponent {
- public static displayName = 'RangeDatePicker';
- public readonly state = {
- startDate: this.props.startDate,
- endDate: this.props.endDate,
- isPopoverOpen: false,
- recentlyUsed: [],
- quickSelectTime: 1,
- quickSelectUnit: 'hours',
- };
-
- public render() {
- const { isLoading, disabled, intl } = this.props;
- const { startDate, endDate } = this.state;
- const quickSelectButton = (
-
-
-
- );
-
- const commonlyUsed = this.renderCommonlyUsed(commonDates);
- const recentlyUsed = this.renderRecentlyUsed([
- ...this.state.recentlyUsed,
- ...this.props.recentlyUsed,
- ]);
-
- const quickSelectPopover = (
-
-
- {this.renderQuickSelect()}
-
- {commonlyUsed}
-
- {recentlyUsed}
-
-
- );
-
- return (
-
- endDate : false}
- fullWidth
- aria-label={intl.formatMessage({
- id: 'xpack.infra.rangeDatePicker.startDateAriaLabel',
- defaultMessage: 'Start date',
- })}
- disabled={disabled}
- shouldCloseOnSelect
- showTimeSelect
- />
- }
- endDateControl={
- endDate : false}
- fullWidth
- disabled={disabled}
- isLoading={isLoading}
- aria-label={intl.formatMessage({
- id: 'xpack.infra.rangeDatePicker.endDateAriaLabel',
- defaultMessage: 'End date',
- })}
- shouldCloseOnSelect
- showTimeSelect
- popperPlacement="top-end"
- />
- }
- />
-
- );
- }
-
- public resetRangeDate(startDate: moment.Moment, endDate: moment.Moment) {
- this.setState({
- ...this.state,
- startDate,
- endDate,
- });
- }
-
- private handleChangeStart = (date: moment.Moment | null) => {
- if (date && this.state.startDate !== date) {
- this.props.onChangeRangeTime(date, this.state.endDate, false);
- this.setState({
- startDate: date,
- });
- }
- };
-
- private handleChangeEnd = (date: moment.Moment | null) => {
- if (date && this.state.endDate !== date) {
- this.props.onChangeRangeTime(this.state.startDate, date, false);
- this.setState({
- endDate: date,
- });
- }
- };
-
- private onButtonClick = () => {
- this.setState({
- isPopoverOpen: !this.state.isPopoverOpen,
- });
- };
-
- private closePopover = (type: string, from?: string, to?: string) => {
- const { startDate, endDate, recentlyUsed } = this.managedStartEndDateFromType(type, from, to);
- this.setState(
- {
- ...this.state,
- isPopoverOpen: false,
- startDate,
- endDate,
- recentlyUsed,
- },
- () => {
- if (type) {
- this.props.onChangeRangeTime(startDate, endDate, true);
- }
- }
- );
- };
-
- private managedStartEndDateFromType(type: string, from?: string, to?: string) {
- const { intl } = this.props;
- let { startDate, endDate } = this.state;
- let recentlyUsed: RecentlyUsed[] = this.state.recentlyUsed;
- let textJustUsed = type;
-
- if (type === 'quick-select') {
- textJustUsed = intl.formatMessage(
- {
- id: 'xpack.infra.rangeDatePicker.lastQuickSelectTimeText',
- defaultMessage: 'Last {quickSelectTime} {quickSelectUnit}',
- },
- {
- quickSelectTime: this.state.quickSelectTime,
- quickSelectUnit:
- this.state.quickSelectTime === 1
- ? get(find(singleLastOptions, { value: this.state.quickSelectUnit }), 'text')
- : get(find(pluralLastOptions, { value: this.state.quickSelectUnit }), 'text'),
- }
- );
- startDate = moment().subtract(this.state.quickSelectTime, this.state
- .quickSelectUnit as moment.unitOfTime.DurationConstructor);
- endDate = moment();
- } else if (type === DatePickerDateOptions.today) {
- startDate = moment().startOf('day');
- endDate = moment()
- .startOf('day')
- .add(24, 'hour');
- } else if (type === DatePickerDateOptions.yesterday) {
- startDate = moment()
- .subtract(1, 'day')
- .startOf('day');
- endDate = moment()
- .subtract(1, 'day')
- .startOf('day')
- .add(24, 'hour');
- } else if (type === DatePickerDateOptions.thisWeek) {
- startDate = moment().startOf('week');
- endDate = moment()
- .startOf('week')
- .add(1, 'week');
- } else if (type === DatePickerDateOptions.weekToDate) {
- startDate = moment().subtract(1, 'week');
- endDate = moment();
- } else if (type === DatePickerDateOptions.thisMonth) {
- startDate = moment().startOf('month');
- endDate = moment()
- .startOf('month')
- .add(1, 'month');
- } else if (type === DatePickerDateOptions.monthToDate) {
- startDate = moment().subtract(1, 'month');
- endDate = moment();
- } else if (type === DatePickerDateOptions.thisYear) {
- startDate = moment().startOf('year');
- endDate = moment()
- .startOf('year')
- .add(1, 'year');
- } else if (type === DatePickerDateOptions.yearToDate) {
- startDate = moment().subtract(1, 'year');
- endDate = moment();
- } else if (type === 'date-range' && to && from) {
- startDate = moment(from);
- endDate = moment(to);
- }
-
- textJustUsed =
- type === 'date-range' || !type ? type : get(find(commonDates, { id: type }), 'label');
-
- if (textJustUsed !== undefined && !find(recentlyUsed, ['text', textJustUsed])) {
- recentlyUsed.unshift({ type, text: textJustUsed });
- recentlyUsed = recentlyUsed.slice(0, 5);
- }
-
- return {
- startDate,
- endDate,
- recentlyUsed,
- };
- }
-
- private renderQuickSelect = () => {
- const { intl } = this.props;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- this.onChange('quickSelectTime', arg);
- }}
- />
-
-
-
-
- {
- this.onChange('quickSelectUnit', arg);
- }}
- />
-
-
-
-
- this.closePopover('quick-select')}
- style={{ minWidth: 0 }}
- >
-
-
-
-
-
-
- );
- };
-
- private onChange = (stateType: string, args: any) => {
- let value = args.currentTarget.value;
-
- if (stateType === 'quickSelectTime' && value !== '') {
- value = parseInt(args.currentTarget.value, 10);
- }
- this.setState({
- ...this.state,
- [stateType]: value,
- });
- };
-
- private renderCommonlyUsed = (recentlyCommonDates: Array<{ id: string; label: any }>) => {
- const links = recentlyCommonDates.map(date => {
- return (
-
- this.closePopover(date.id)}>{date.label}
-
- );
- });
-
- return (
-
-
-
-
-
-
-
-
-
- {links}
-
-
-
- );
- };
-
- private renderRecentlyUsed = (recentDates: RecentlyUsed[]) => {
- const links = recentDates.map((date: RecentlyUsed) => {
- let dateRange;
- let dateLink = (
- this.closePopover(date.type)}>{dateRange || date.text}
- );
- if (typeof date.text !== 'string') {
- dateRange = `${date.text[0]} – ${date.text[1]}`;
- dateLink = (
- this.closePopover(date.type, date.text[0], date.text[1])}>
- {dateRange || date.type}
-
- );
- }
-
- return (
-
- {dateLink}
-
- );
- });
-
- return (
-
-
-
-
-
-
-
-
-
- {links}
-
-
-
- );
- };
- }
-);
diff --git a/x-pack/plugins/infra/public/containers/metrics/metrics_time.test.tsx b/x-pack/plugins/infra/public/containers/metrics/metrics_time.test.tsx
new file mode 100644
index 00000000000000..315c9e2afd8aba
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/metrics/metrics_time.test.tsx
@@ -0,0 +1,98 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { mountHook } from 'test_utils/enzyme_helpers';
+
+import { useMetricsTime } from './with_metrics_time';
+
+describe('useMetricsTime hook', () => {
+ describe('timeRange state', () => {
+ it('has a default value', () => {
+ const { getLastHookValue } = mountHook(() => useMetricsTime().timeRange);
+ const hookValue = getLastHookValue();
+ expect(hookValue).toHaveProperty('from');
+ expect(hookValue).toHaveProperty('to');
+ expect(hookValue.interval).toBe('>=1m');
+ });
+
+ it('can be updated', () => {
+ const { act, getLastHookValue } = mountHook(() => useMetricsTime());
+
+ const timeRange = {
+ from: 12345,
+ to: 123456,
+ interval: '>=2m',
+ };
+
+ act(({ setTimeRange }) => {
+ setTimeRange(timeRange);
+ });
+
+ expect(getLastHookValue().timeRange).toEqual(timeRange);
+ });
+ });
+
+ describe('AutoReloading state', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ it('has a default value', () => {
+ const { getLastHookValue } = mountHook(() => useMetricsTime().isAutoReloading);
+ expect(getLastHookValue()).toBe(false);
+ });
+
+ it('can be updated', () => {
+ const { act, getLastHookValue } = mountHook(() => useMetricsTime());
+
+ act(({ setAutoReload }) => {
+ setAutoReload(true);
+ });
+
+ expect(getLastHookValue().isAutoReloading).toBe(true);
+ });
+
+ it('sets up an interval when turned on', () => {
+ const { act } = mountHook(() => useMetricsTime());
+ const refreshInterval = 10000;
+
+ act(({ setAutoReload, setRefreshInterval }) => {
+ setRefreshInterval(refreshInterval);
+ setAutoReload(true);
+ jest.runOnlyPendingTimers();
+ });
+
+ expect(setInterval).toHaveBeenCalledTimes(1);
+ expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), refreshInterval);
+ });
+
+ it('updates the time range by RANGE each interval', () => {
+ const { act, getLastHookValue } = mountHook(() => useMetricsTime());
+ const from = 100;
+ const to = 300;
+ const RANGE = 200;
+
+ act(({ setAutoReload, setTimeRange }) => {
+ setAutoReload(true);
+
+ setTimeRange({
+ from,
+ to,
+ interval: '>=1m',
+ });
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(6000);
+ });
+
+ const timeRange = getLastHookValue().timeRange;
+ expect(timeRange.from).toBeGreaterThan(from);
+ expect(timeRange.to).toBeGreaterThan(to);
+ expect(timeRange.to - timeRange.from).toBe(RANGE);
+ });
+ });
+});
diff --git a/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx b/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx
index a6c7e94f2be160..ac7bd640a68acb 100644
--- a/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx
+++ b/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx
@@ -4,68 +4,134 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-
-import { metricTimeActions, metricTimeSelectors, State } from '../../store';
-import { asChildFunctionRenderer } from '../../utils/typed_react';
-import { bindPlainActionCreators } from '../../utils/typed_redux';
+import createContainer from 'constate-latest';
+import moment from 'moment';
+import React, { useCallback, useContext, useEffect, useState } from 'react';
+import { InfraTimerangeInput } from '../../graphql/types';
+import { useInterval } from '../../hooks/use_interval';
import { replaceStateKeyInQueryString, UrlStateContainer } from '../../utils/url_state';
-export const withMetricsTime = connect(
- (state: State) => ({
- currentTimeRange: metricTimeSelectors.selectRangeTime(state),
- isAutoReloading: metricTimeSelectors.selectIsAutoReloading(state),
- urlState: selectTimeUrlState(state),
- }),
- bindPlainActionCreators({
- setRangeTime: metricTimeActions.setRangeTime,
- startMetricsAutoReload: metricTimeActions.startMetricsAutoReload,
- stopMetricsAutoReload: metricTimeActions.stopMetricsAutoReload,
- })
-);
+interface MetricsTimeState {
+ timeRange: InfraTimerangeInput;
+ setTimeRange: (timeRange: InfraTimerangeInput) => void;
+ refreshInterval: number;
+ setRefreshInterval: (refreshInterval: number) => void;
+ isAutoReloading: boolean;
+ setAutoReload: (isAutoReloading: boolean) => void;
+}
+
+export const useMetricsTime = () => {
+ const [isAutoReloading, setAutoReload] = useState(false);
+ const [refreshInterval, setRefreshInterval] = useState(5000);
+ const [timeRange, setTimeRange] = useState({
+ from: moment()
+ .subtract(1, 'hour')
+ .valueOf(),
+ to: moment().valueOf(),
+ interval: '>=1m',
+ });
+
+ const setTimeRangeToNow = useCallback(
+ () => {
+ const range = timeRange.to - timeRange.from;
+ setTimeRange({
+ from: moment()
+ .subtract(range, 'ms')
+ .valueOf(),
+ to: moment().valueOf(),
+ interval: '>=1m',
+ });
+ },
+ [timeRange.from, timeRange.to]
+ );
+
+ useInterval(setTimeRangeToNow, isAutoReloading ? refreshInterval : null);
+
+ useEffect(
+ () => {
+ if (isAutoReloading) {
+ setTimeRangeToNow();
+ }
+ },
+ [isAutoReloading]
+ );
-export const WithMetricsTime = asChildFunctionRenderer(withMetricsTime, {
- onCleanup: ({ stopMetricsAutoReload }) => stopMetricsAutoReload(),
-});
+ return {
+ timeRange,
+ setTimeRange,
+ refreshInterval,
+ setRefreshInterval,
+ isAutoReloading,
+ setAutoReload,
+ };
+};
+
+export const MetricsTimeContainer = createContainer(useMetricsTime);
+
+interface WithMetricsTimeProps {
+ children: (args: MetricsTimeState) => React.ReactElement;
+}
+export const WithMetricsTime: React.FunctionComponent = ({
+ children,
+}: WithMetricsTimeProps) => {
+ const metricsTimeState = useContext(MetricsTimeContainer.Context);
+ return children({ ...metricsTimeState });
+};
/**
* Url State
*/
-interface MetricTimeUrlState {
- time?: ReturnType;
- autoReload?: ReturnType;
+interface MetricsTimeUrlState {
+ time?: MetricsTimeState['timeRange'];
+ autoReload?: boolean;
+ refreshInterval?: number;
}
export const WithMetricsTimeUrlState = () => (
- {({ setRangeTime, startMetricsAutoReload, stopMetricsAutoReload, urlState }) => (
+ {({
+ timeRange,
+ setTimeRange,
+ refreshInterval,
+ setRefreshInterval,
+ isAutoReloading,
+ setAutoReload,
+ }) => (
{
if (newUrlState && newUrlState.time) {
- setRangeTime(newUrlState.time);
+ setTimeRange(newUrlState.time);
}
if (newUrlState && newUrlState.autoReload) {
- startMetricsAutoReload();
+ setAutoReload(true);
} else if (
newUrlState &&
typeof newUrlState.autoReload !== 'undefined' &&
!newUrlState.autoReload
) {
- stopMetricsAutoReload();
+ setAutoReload(false);
+ }
+ if (newUrlState && newUrlState.refreshInterval) {
+ setRefreshInterval(newUrlState.refreshInterval);
}
}}
onInitialize={initialUrlState => {
if (initialUrlState && initialUrlState.time) {
- setRangeTime(initialUrlState.time);
+ setTimeRange(initialUrlState.time);
}
if (initialUrlState && initialUrlState.autoReload) {
- startMetricsAutoReload();
+ setAutoReload(true);
+ }
+ if (initialUrlState && initialUrlState.refreshInterval) {
+ setRefreshInterval(initialUrlState.refreshInterval);
}
}}
/>
@@ -73,20 +139,12 @@ export const WithMetricsTimeUrlState = () => (
);
-const selectTimeUrlState = createSelector(
- metricTimeSelectors.selectRangeTime,
- metricTimeSelectors.selectIsAutoReloading,
- (time, autoReload) => ({
- time,
- autoReload,
- })
-);
-
-const mapToUrlState = (value: any): MetricTimeUrlState | undefined =>
+const mapToUrlState = (value: any): MetricsTimeUrlState | undefined =>
value
? {
time: mapToTimeUrlState(value.time),
autoReload: mapToAutoReloadUrlState(value.autoReload),
+ refreshInterval: mapToRefreshInterval(value.refreshInterval),
}
: undefined;
@@ -95,10 +153,12 @@ const mapToTimeUrlState = (value: any) =>
const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined);
+const mapToRefreshInterval = (value: any) => (typeof value === 'number' ? value : undefined);
+
export const replaceMetricTimeInQueryString = (from: number, to: number) =>
Number.isNaN(from) || Number.isNaN(to)
? (value: string) => value
- : replaceStateKeyInQueryString('metricTime', {
+ : replaceStateKeyInQueryString('metricTime', {
autoReload: false,
time: {
interval: '>=1m',
diff --git a/x-pack/plugins/infra/public/containers/with_kibana_chrome.tsx b/x-pack/plugins/infra/public/containers/with_kibana_chrome.tsx
index c5a7b4c1ae0a2b..eca85874e5966d 100644
--- a/x-pack/plugins/infra/public/containers/with_kibana_chrome.tsx
+++ b/x-pack/plugins/infra/public/containers/with_kibana_chrome.tsx
@@ -20,7 +20,6 @@ interface WithKibanaChromeProps {
interface WithKibanaChromeState {
basePath: string;
- dateFormat?: string;
}
export class WithKibanaChrome extends React.Component<
@@ -29,7 +28,6 @@ export class WithKibanaChrome extends React.Component<
> {
public state: WithKibanaChromeState = {
basePath: chrome.getBasePath(),
- dateFormat: chrome.getUiSettingsClient().get('dateFormat'),
};
public render() {
diff --git a/x-pack/plugins/infra/public/hooks/use_interval.ts b/x-pack/plugins/infra/public/hooks/use_interval.ts
new file mode 100644
index 00000000000000..4e063d6d51ce32
--- /dev/null
+++ b/x-pack/plugins/infra/public/hooks/use_interval.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 { useEffect, useRef } from 'react';
+
+export function useInterval(callback: () => void, delay: number | null) {
+ const savedCallback = useRef(callback);
+
+ useEffect(
+ () => {
+ savedCallback.current = callback;
+ },
+ [callback]
+ );
+
+ useEffect(
+ () => {
+ function tick() {
+ savedCallback.current();
+ }
+
+ if (delay !== null) {
+ const id = setInterval(tick, delay);
+ return () => clearInterval(id);
+ }
+ },
+ [delay]
+ );
+}
diff --git a/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts
index 71dea6344855f4..beb912373dbff7 100644
--- a/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts
+++ b/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts
@@ -26,10 +26,7 @@ const BREADCRUMBS_ELEMENT_ID = 'react-infra-breadcrumbs';
export class InfraKibanaFrameworkAdapter implements InfraFrameworkAdapter {
public appState: object;
- public dateFormat?: string;
public kbnVersion?: string;
- public darkMode?: boolean;
- public scaledDateFormat?: string;
public timezone?: string;
private adapterService: KibanaAdapterServiceProvider;
@@ -134,13 +131,6 @@ export class InfraKibanaFrameworkAdapter implements InfraFrameworkAdapter {
) => {
this.timezone = Private(this.timezoneProvider)();
this.kbnVersion = kbnVersion;
- this.dateFormat = config.get('dateFormat');
- try {
- this.darkMode = config.get('theme:darkMode');
- } catch (e) {
- this.darkMode = false;
- }
- this.scaledDateFormat = config.get('dateFormat:scaled');
});
uiRoutes.enable();
diff --git a/x-pack/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts b/x-pack/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts
index da56c7b97b226d..f1f08f6ffbf348 100644
--- a/x-pack/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts
+++ b/x-pack/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts
@@ -8,9 +8,7 @@ import { InfraFrameworkAdapter } from '../../lib';
export class InfraTestingFrameworkAdapter implements InfraFrameworkAdapter {
public appState?: object;
- public dateFormat?: string;
public kbnVersion?: string;
- public scaledDateFormat?: string;
public timezone?: string;
constructor() {
diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts
index 759863ce878736..1f1cbe9c0effd3 100644
--- a/x-pack/plugins/infra/public/lib/lib.ts
+++ b/x-pack/plugins/infra/public/lib/lib.ts
@@ -33,11 +33,8 @@ export type InfraApolloClient = ApolloClient;
export interface InfraFrameworkAdapter {
// Insstance vars
appState?: object;
- dateFormat?: string;
kbnVersion?: string;
- scaledDateFormat?: string;
timezone?: string;
- darkMode?: boolean;
// Methods
setUISettings(key: string, value: any): void;
diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx
index d7c3403391086a..84180a84dfe396 100644
--- a/x-pack/plugins/infra/public/pages/metrics/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx
@@ -31,6 +31,7 @@ import { SourceConfigurationFlyout } from '../../components/source_configuration
import { WithMetadata } from '../../containers/metadata/with_metadata';
import { WithMetrics } from '../../containers/metrics/with_metrics';
import {
+ MetricsTimeContainer,
WithMetricsTime,
WithMetricsTimeUrlState,
} from '../../containers/metrics/with_metrics_time';
@@ -88,154 +89,159 @@ export const MetricDetail = withTheme(
const layouts = layoutCreator(this.props.theme);
return (
-
- {({ sourceId }) => (
-
- {({
- currentTimeRange,
- isAutoReloading,
- setRangeTime,
- startMetricsAutoReload,
- stopMetricsAutoReload,
- }) => (
-
- {({ name, filteredLayouts, loading: metadataLoading }) => {
- const breadcrumbs = [
- {
- href: '#/',
- text: intl.formatMessage({
- id: 'xpack.infra.header.infrastructureTitle',
- defaultMessage: 'Infrastructure',
- }),
- },
- { text: name },
- ];
- return (
-
-
-
-
-
-
-
- {({ metrics, error, loading, refetch }) => {
- if (error) {
- const invalidNodeError = error.graphQLErrors.some(
- (err: GraphQLFormattedError) =>
- err.code === InfraMetricsErrorCodes.invalid_node
- );
+
+
+ {({ sourceId }) => (
+
+ {({
+ timeRange,
+ setTimeRange,
+ refreshInterval,
+ setRefreshInterval,
+ isAutoReloading,
+ setAutoReload,
+ }) => (
+
+ {({ name, filteredLayouts, loading: metadataLoading }) => {
+ const breadcrumbs = [
+ {
+ href: '#/',
+ text: intl.formatMessage({
+ id: 'xpack.infra.header.infrastructureTitle',
+ defaultMessage: 'Infrastructure',
+ }),
+ },
+ { text: name },
+ ];
+ return (
+
+
+
+
+
+
+
+ {({ metrics, error, loading, refetch }) => {
+ if (error) {
+ const invalidNodeError = error.graphQLErrors.some(
+ (err: GraphQLFormattedError) =>
+ err.code === InfraMetricsErrorCodes.invalid_node
+ );
+ return (
+ <>
+
+ intl.formatMessage(
+ {
+ id:
+ 'xpack.infra.metricDetailPage.documentTitleError',
+ defaultMessage: '{previousTitle} | Uh oh',
+ },
+ {
+ previousTitle,
+ }
+ )
+ }
+ />
+ {invalidNodeError ? (
+
+ ) : (
+
+ )}
+ >
+ );
+ }
return (
- <>
-
- intl.formatMessage(
- {
- id: 'xpack.infra.metricDetailPage.documentTitleError',
- defaultMessage: '{previousTitle} | Uh oh',
- },
- {
- previousTitle,
- }
- )
- }
+
+
- {invalidNodeError ? (
-
- ) : (
-
- )}
- >
- );
- }
- return (
-
-
-
- {({ measureRef, bounds: { width = 0 } }) => {
- return (
-
-
-
-
-
-
-
- {name}
-
-
-
-
-
-
+
+ {({ measureRef, bounds: { width = 0 } }) => {
+ return (
+
+
+
+
+
+
+
+ {name}
+
+
+
+
+
+
-
- 0 && isAutoReloading
- ? false
- : loading
- }
- refetch={refetch}
- onChangeRangeTime={setRangeTime}
- isLiveStreaming={isAutoReloading}
- stopLiveStreaming={stopMetricsAutoReload}
- />
-
-
-
- );
- }}
-
-
- );
- }}
-
-
-
- );
- }}
-
- )}
-
- )}
-
+
+ 0 && isAutoReloading
+ ? false
+ : loading
+ }
+ refetch={refetch}
+ onChangeRangeTime={setTimeRange}
+ isLiveStreaming={isAutoReloading}
+ stopLiveStreaming={() => setAutoReload(false)}
+ />
+
+
+
+ );
+ }}
+
+
+ );
+ }}
+
+
+
+ );
+ }}
+
+ )}
+
+ )}
+
+
);
}
diff --git a/x-pack/plugins/infra/public/store/actions.ts b/x-pack/plugins/infra/public/store/actions.ts
index 34b65aa1068d69..103a779e59f2a5 100644
--- a/x-pack/plugins/infra/public/store/actions.ts
+++ b/x-pack/plugins/infra/public/store/actions.ts
@@ -7,7 +7,6 @@
export {
logFilterActions,
logPositionActions,
- metricTimeActions,
waffleFilterActions,
waffleTimeActions,
waffleOptionsActions,
diff --git a/x-pack/plugins/infra/public/store/local/actions.ts b/x-pack/plugins/infra/public/store/local/actions.ts
index ad1dbd9b289661..19ee0d1f4780fb 100644
--- a/x-pack/plugins/infra/public/store/local/actions.ts
+++ b/x-pack/plugins/infra/public/store/local/actions.ts
@@ -6,7 +6,6 @@
export { logFilterActions } from './log_filter';
export { logPositionActions } from './log_position';
-export { metricTimeActions } from './metric_time';
export { waffleFilterActions } from './waffle_filter';
export { waffleTimeActions } from './waffle_time';
export { waffleOptionsActions } from './waffle_options';
diff --git a/x-pack/plugins/infra/public/store/local/epic.ts b/x-pack/plugins/infra/public/store/local/epic.ts
index 274a74b1627c5b..4cfac85f00b15b 100644
--- a/x-pack/plugins/infra/public/store/local/epic.ts
+++ b/x-pack/plugins/infra/public/store/local/epic.ts
@@ -7,12 +7,7 @@
import { combineEpics } from 'redux-observable';
import { createLogPositionEpic } from './log_position';
-import { createMetricTimeEpic } from './metric_time';
import { createWaffleTimeEpic } from './waffle_time';
export const createLocalEpic = () =>
- combineEpics(
- createLogPositionEpic(),
- createWaffleTimeEpic(),
- createMetricTimeEpic()
- );
+ combineEpics(createLogPositionEpic(), createWaffleTimeEpic());
diff --git a/x-pack/plugins/infra/public/store/local/metric_time/actions.ts b/x-pack/plugins/infra/public/store/local/metric_time/actions.ts
deleted file mode 100644
index 374cb9bcd87aa2..00000000000000
--- a/x-pack/plugins/infra/public/store/local/metric_time/actions.ts
+++ /dev/null
@@ -1,21 +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 actionCreatorFactory from 'typescript-fsa';
-
-const actionCreator = actionCreatorFactory('x-pack/infra/local/metric_time');
-
-export interface MetricRangeTimeState {
- to: number;
- from: number;
- interval: string;
-}
-
-export const setRangeTime = actionCreator('SET_RANGE_TIME');
-
-export const startMetricsAutoReload = actionCreator('START_METRICS_AUTO_RELOAD');
-
-export const stopMetricsAutoReload = actionCreator('STOP_METRICS_AUTO_RELOAD');
diff --git a/x-pack/plugins/infra/public/store/local/metric_time/epic.ts b/x-pack/plugins/infra/public/store/local/metric_time/epic.ts
deleted file mode 100644
index aaecdc42a215b2..00000000000000
--- a/x-pack/plugins/infra/public/store/local/metric_time/epic.ts
+++ /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 moment from 'moment';
-import { Action } from 'redux';
-import { Epic } from 'redux-observable';
-import { timer } from 'rxjs';
-import { exhaustMap, filter, map, takeUntil, withLatestFrom } from 'rxjs/operators';
-
-import { setRangeTime, startMetricsAutoReload, stopMetricsAutoReload } from './actions';
-
-interface MetricTimeEpicDependencies {
- selectMetricTimeUpdatePolicyInterval: (state: State) => number | null;
- selectMetricRangeFromTimeRange: (state: State) => number | null;
-}
-
-export const createMetricTimeEpic = (): Epic<
- Action,
- Action,
- State,
- MetricTimeEpicDependencies
-> => (
- action$,
- state$,
- { selectMetricTimeUpdatePolicyInterval, selectMetricRangeFromTimeRange }
-) => {
- const updateInterval$ = state$.pipe(
- map(selectMetricTimeUpdatePolicyInterval),
- filter(isNotNull)
- );
-
- const range$ = state$.pipe(
- map(selectMetricRangeFromTimeRange),
- filter(isNotNull)
- );
-
- return action$.pipe(
- filter(startMetricsAutoReload.match),
- withLatestFrom(updateInterval$, range$),
- exhaustMap(([action, updateInterval, range]) =>
- timer(0, updateInterval).pipe(
- map(() =>
- setRangeTime({
- from: moment()
- .subtract(range, 'ms')
- .valueOf(),
- to: moment().valueOf(),
- interval: '1m',
- })
- ),
- takeUntil(action$.pipe(filter(stopMetricsAutoReload.match)))
- )
- )
- );
-};
-
-const isNotNull = (value: T | null): value is T => value !== null;
diff --git a/x-pack/plugins/infra/public/store/local/metric_time/reducer.ts b/x-pack/plugins/infra/public/store/local/metric_time/reducer.ts
deleted file mode 100644
index 00a4d7d311d0d2..00000000000000
--- a/x-pack/plugins/infra/public/store/local/metric_time/reducer.ts
+++ /dev/null
@@ -1,63 +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 moment from 'moment';
-import { combineReducers } from 'redux';
-import { reducerWithInitialState } from 'typescript-fsa-reducers/dist';
-
-import {
- MetricRangeTimeState,
- setRangeTime,
- startMetricsAutoReload,
- stopMetricsAutoReload,
-} from './actions';
-
-interface ManualTimeUpdatePolicy {
- policy: 'manual';
-}
-
-interface IntervalTimeUpdatePolicy {
- policy: 'interval';
- interval: number;
-}
-
-type TimeUpdatePolicy = ManualTimeUpdatePolicy | IntervalTimeUpdatePolicy;
-
-export interface MetricTimeState {
- timeRange: MetricRangeTimeState;
- updatePolicy: TimeUpdatePolicy;
-}
-
-export const initialMetricTimeState: MetricTimeState = {
- timeRange: {
- from: moment()
- .subtract(1, 'hour')
- .valueOf(),
- to: moment().valueOf(),
- interval: '>=1m',
- },
- updatePolicy: {
- policy: 'manual',
- },
-};
-
-const timeRangeReducer = reducerWithInitialState(initialMetricTimeState.timeRange).case(
- setRangeTime,
- (state, { to, from }) => ({ ...state, to, from })
-);
-
-const updatePolicyReducer = reducerWithInitialState(initialMetricTimeState.updatePolicy)
- .case(startMetricsAutoReload, () => ({
- policy: 'interval',
- interval: 5000,
- }))
- .case(stopMetricsAutoReload, () => ({
- policy: 'manual',
- }));
-
-export const metricTimeReducer = combineReducers({
- timeRange: timeRangeReducer,
- updatePolicy: updatePolicyReducer,
-});
diff --git a/x-pack/plugins/infra/public/store/local/metric_time/selectors.ts b/x-pack/plugins/infra/public/store/local/metric_time/selectors.ts
deleted file mode 100644
index cac7ac2edca05e..00000000000000
--- a/x-pack/plugins/infra/public/store/local/metric_time/selectors.ts
+++ /dev/null
@@ -1,19 +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 { MetricTimeState } from './reducer';
-
-export const selectRangeTime = (state: MetricTimeState) => state.timeRange;
-
-export const selectIsAutoReloading = (state: MetricTimeState) =>
- state.updatePolicy.policy === 'interval';
-
-export const selectTimeUpdatePolicyInterval = (state: MetricTimeState) =>
- state.updatePolicy.policy === 'interval' ? state.updatePolicy.interval : null;
-
-export const selectRangeFromTimeRange = (state: MetricTimeState) => {
- const { to, from } = state.timeRange;
- return to - from;
-};
diff --git a/x-pack/plugins/infra/public/store/local/reducer.ts b/x-pack/plugins/infra/public/store/local/reducer.ts
index 675005ca7a85cd..ef9327454d45de 100644
--- a/x-pack/plugins/infra/public/store/local/reducer.ts
+++ b/x-pack/plugins/infra/public/store/local/reducer.ts
@@ -9,7 +9,6 @@ import { combineReducers } from 'redux';
import { initialLogFilterState, logFilterReducer, LogFilterState } from './log_filter';
import { flyoutOptionsReducer, FlyoutOptionsState, initialFlyoutOptionsState } from './log_flyout';
import { initialLogPositionState, logPositionReducer, LogPositionState } from './log_position';
-import { initialMetricTimeState, metricTimeReducer, MetricTimeState } from './metric_time';
import { initialWaffleFilterState, waffleFilterReducer, WaffleFilterState } from './waffle_filter';
import {
initialWaffleOptionsState,
@@ -21,7 +20,6 @@ import { initialWaffleTimeState, waffleTimeReducer, WaffleTimeState } from './wa
export interface LocalState {
logFilter: LogFilterState;
logPosition: LogPositionState;
- metricTime: MetricTimeState;
waffleFilter: WaffleFilterState;
waffleTime: WaffleTimeState;
waffleMetrics: WaffleOptionsState;
@@ -31,7 +29,6 @@ export interface LocalState {
export const initialLocalState: LocalState = {
logFilter: initialLogFilterState,
logPosition: initialLogPositionState,
- metricTime: initialMetricTimeState,
waffleFilter: initialWaffleFilterState,
waffleTime: initialWaffleTimeState,
waffleMetrics: initialWaffleOptionsState,
@@ -41,7 +38,6 @@ export const initialLocalState: LocalState = {
export const localReducer = combineReducers({
logFilter: logFilterReducer,
logPosition: logPositionReducer,
- metricTime: metricTimeReducer,
waffleFilter: waffleFilterReducer,
waffleTime: waffleTimeReducer,
waffleMetrics: waffleOptionsReducer,
diff --git a/x-pack/plugins/infra/public/store/local/selectors.ts b/x-pack/plugins/infra/public/store/local/selectors.ts
index a7598c83267465..9d7ef1c5c0e7b1 100644
--- a/x-pack/plugins/infra/public/store/local/selectors.ts
+++ b/x-pack/plugins/infra/public/store/local/selectors.ts
@@ -8,7 +8,6 @@ import { globalizeSelectors } from '../../utils/typed_redux';
import { logFilterSelectors as innerLogFilterSelectors } from './log_filter';
import { flyoutOptionsSelectors as innerFlyoutOptionsSelectors } from './log_flyout';
import { logPositionSelectors as innerLogPositionSelectors } from './log_position';
-import { metricTimeSelectors as innerMetricTimeSelectors } from './metric_time';
import { LocalState } from './reducer';
import { waffleFilterSelectors as innerWaffleFilterSelectors } from './waffle_filter';
import { waffleOptionsSelectors as innerWaffleOptionsSelectors } from './waffle_options';
@@ -24,11 +23,6 @@ export const logPositionSelectors = globalizeSelectors(
innerLogPositionSelectors
);
-export const metricTimeSelectors = globalizeSelectors(
- (state: LocalState) => state.metricTime,
- innerMetricTimeSelectors
-);
-
export const waffleFilterSelectors = globalizeSelectors(
(state: LocalState) => state.waffleFilter,
innerWaffleFilterSelectors
diff --git a/x-pack/plugins/infra/public/store/selectors.ts b/x-pack/plugins/infra/public/store/selectors.ts
index b4e71cfdbd47cf..4b342cc56968c8 100644
--- a/x-pack/plugins/infra/public/store/selectors.ts
+++ b/x-pack/plugins/infra/public/store/selectors.ts
@@ -12,7 +12,6 @@ import {
flyoutOptionsSelectors as localFlyoutOptionsSelectors,
logFilterSelectors as localLogFilterSelectors,
logPositionSelectors as localLogPositionSelectors,
- metricTimeSelectors as localMetricTimeSelectors,
waffleFilterSelectors as localWaffleFilterSelectors,
waffleOptionsSelectors as localWaffleOptionsSelectors,
waffleTimeSelectors as localWaffleTimeSelectors,
@@ -28,7 +27,6 @@ const selectLocal = (state: State) => state.local;
export const logFilterSelectors = globalizeSelectors(selectLocal, localLogFilterSelectors);
export const logPositionSelectors = globalizeSelectors(selectLocal, localLogPositionSelectors);
-export const metricTimeSelectors = globalizeSelectors(selectLocal, localMetricTimeSelectors);
export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors);
export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors);
export const waffleOptionsSelectors = globalizeSelectors(selectLocal, localWaffleOptionsSelectors);
diff --git a/x-pack/plugins/infra/public/store/store.ts b/x-pack/plugins/infra/public/store/store.ts
index 9a6c3dfcf573ba..bdddcf7a4cc253 100644
--- a/x-pack/plugins/infra/public/store/store.ts
+++ b/x-pack/plugins/infra/public/store/store.ts
@@ -15,7 +15,6 @@ import {
logEntriesSelectors,
logFilterSelectors,
logPositionSelectors,
- metricTimeSelectors,
reducer,
State,
waffleTimeSelectors,
@@ -49,8 +48,6 @@ export function createStore({ apolloClient, observableApi }: StoreDependencies)
selectLogTargetPosition: logPositionSelectors.selectTargetPosition,
selectVisibleLogMidpointOrTarget: logPositionSelectors.selectVisibleMidpointOrTarget,
selectWaffleTimeUpdatePolicyInterval: waffleTimeSelectors.selectTimeUpdatePolicyInterval,
- selectMetricTimeUpdatePolicyInterval: metricTimeSelectors.selectTimeUpdatePolicyInterval,
- selectMetricRangeFromTimeRange: metricTimeSelectors.selectRangeFromTimeRange,
};
const epicMiddleware = createEpicMiddleware(
diff --git a/x-pack/plugins/infra/public/utils/use_kibana_ui_setting.ts b/x-pack/plugins/infra/public/utils/use_kibana_ui_setting.ts
new file mode 100644
index 00000000000000..76fff4f00b71b1
--- /dev/null
+++ b/x-pack/plugins/infra/public/utils/use_kibana_ui_setting.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useCallback, useMemo } from 'react';
+
+import { getNewPlatform } from 'ui/new_platform';
+import { useObservable } from './use_observable';
+
+/**
+ * This hook behaves like a `useState` hook in that it provides a requested
+ * setting value (with an optional default) from the Kibana UI settings (also
+ * known as "advanced settings") and a setter to change that setting:
+ *
+ * ```
+ * const [darkMode, setDarkMode] = useKibanaUiSetting('theme:darkMode');
+ * ```
+ *
+ * This is not just a static consumption of the value, but will reactively
+ * update when the underlying setting subscription of the `UiSettingsClient`
+ * notifies of a change.
+ *
+ * Unlike the `useState`, it doesn't give type guarantees for the value,
+ * because the underlying `UiSettingsClient` doesn't support that.
+ */
+export const useKibanaUiSetting = (key: string, defaultValue?: any) => {
+ const uiSettingsClient = useMemo(() => getNewPlatform().setup.core.uiSettings, [getNewPlatform]);
+
+ const uiSetting$ = useMemo(() => uiSettingsClient.get$(key, defaultValue), [uiSettingsClient]);
+ const uiSetting = useObservable(uiSetting$);
+
+ const setUiSetting = useCallback((value: any) => uiSettingsClient.set(key, value), [
+ uiSettingsClient,
+ ]);
+
+ return [uiSetting, setUiSetting];
+};
diff --git a/x-pack/plugins/infra/public/utils/use_observable.ts b/x-pack/plugins/infra/public/utils/use_observable.ts
new file mode 100644
index 00000000000000..08255b3e3320be
--- /dev/null
+++ b/x-pack/plugins/infra/public/utils/use_observable.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useEffect, useState } from 'react';
+import { Observable } from 'rxjs';
+
+export function useObservable(observable$: Observable): T | undefined;
+export function useObservable(observable$: Observable, initialValue: T): T;
+export function useObservable(observable$: Observable, initialValue?: T): T | undefined {
+ const [value, update] = useState(initialValue);
+
+ useEffect(
+ () => {
+ const s = observable$.subscribe(update);
+ return () => s.unsubscribe();
+ },
+ [observable$]
+ );
+
+ return value;
+}
diff --git a/x-pack/plugins/infra/types/eui_experimental.d.ts b/x-pack/plugins/infra/types/eui_experimental.d.ts
index da03b34c6b98ca..8d5086eaa4d92b 100644
--- a/x-pack/plugins/infra/types/eui_experimental.d.ts
+++ b/x-pack/plugins/infra/types/eui_experimental.d.ts
@@ -59,6 +59,7 @@ declare module '@elastic/eui/lib/experimental' {
value: any;
}
type EuiCrosshairXProps = CommonProps & {
+ marginLeft?: number;
seriesNames: string[];
titleFormat?: (dataPoints: EuiDataPoint[]) => EuiFormattedValue | undefined;
itemsFormat?: (dataPoints: EuiDataPoint[]) => EuiFormattedValue[];
diff --git a/x-pack/plugins/maps/public/actions/store_actions.js b/x-pack/plugins/maps/public/actions/store_actions.js
index 4728447a251363..783b5c2e66a190 100644
--- a/x-pack/plugins/maps/public/actions/store_actions.js
+++ b/x-pack/plugins/maps/public/actions/store_actions.js
@@ -156,12 +156,13 @@ export function addLayer(layerDescriptor) {
};
}
-export function setLayerErrorStatus(layerId, errorMessage) {
+function setLayerDataLoadErrorStatus(layerId, errorMessage) {
return dispatch => {
dispatch({
type: SET_LAYER_ERROR_STATUS,
+ isInErrorState: errorMessage !== null,
layerId,
- errorMessage,
+ errorMessage
});
};
}
@@ -403,7 +404,7 @@ export function updateSourceDataRequest(layerId, newData) {
}
export function endDataLoad(layerId, dataId, requestToken, data, meta) {
- return (dispatch) => {
+ return async (dispatch) => {
dispatch(clearTooltipStateForLayer(layerId));
dispatch({
type: LAYER_DATA_LOAD_ENDED,
@@ -413,6 +414,10 @@ export function endDataLoad(layerId, dataId, requestToken, data, meta) {
meta,
requestToken
});
+ //Clear any data-load errors when there is a succesful data return.
+ //Co this on end-data-load iso at start-data-load to avoid blipping the error status between true/false.
+ //This avoids jitter in the warning icon of the TOC when the requests continues to return errors.
+ dispatch(setLayerDataLoadErrorStatus(layerId, null));
};
}
@@ -426,7 +431,7 @@ export function onDataLoadError(layerId, dataId, requestToken, errorMessage) {
requestToken,
});
- dispatch(setLayerErrorStatus(layerId, errorMessage));
+ dispatch(setLayerDataLoadErrorStatus(layerId, errorMessage));
};
}
diff --git a/x-pack/plugins/maps/public/components/layer_panel/filter_editor/filter_editor.js b/x-pack/plugins/maps/public/components/layer_panel/filter_editor/filter_editor.js
index c260b67d7de71f..6d5cfe28c490ad 100644
--- a/x-pack/plugins/maps/public/components/layer_panel/filter_editor/filter_editor.js
+++ b/x-pack/plugins/maps/public/components/layer_panel/filter_editor/filter_editor.js
@@ -174,9 +174,7 @@ export class FilterEditor extends Component {
/>
-
{this._renderQuery()}
-
{this._renderQueryPopover()}
);
diff --git a/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap b/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap
new file mode 100644
index 00000000000000..2ac044bf0fa14c
--- /dev/null
+++ b/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap
@@ -0,0 +1,237 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FeatureTooltip should not show close button and not show filter button 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`FeatureTooltip should show both filter buttons and close button 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ foo
+
+
+
+
+
+
+
+
+
+
+
+
+ foo
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`FeatureTooltip should show close button, but not filter button 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`FeatureTooltip should show only filter button for filterable properties 1`] = `
+
+
+
+
+
+
+
+ foo
+
+
+
+
+
+
+
+
+
+
+
+
+ foo
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/maps/public/components/map/feature_tooltip.js b/x-pack/plugins/maps/public/components/map/feature_tooltip.js
index 9410800e48c2fe..0b0a902e845e96 100644
--- a/x-pack/plugins/maps/public/components/map/feature_tooltip.js
+++ b/x-pack/plugins/maps/public/components/map/feature_tooltip.js
@@ -11,52 +11,93 @@ import { i18n } from '@kbn/i18n';
export class FeatureTooltip extends React.Component {
+ _renderFilterButton(tooltipProperty) {
+ if (!this.props.showFilterButtons || !tooltipProperty.isFilterable()) {
+ return null;
+ }
- _renderProperties() {
- return Object.keys(this.props.properties).map(propertyName => {
+ return (
+
+ {
+ this.props.closeTooltip();
+ const filterAction = tooltipProperty.getFilterAction();
+ filterAction();
+ }}
+ aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', {
+ defaultMessage: 'Filter on property'
+ })}
+ className="mapFeatureTooltipFilterButton"
+ />
+
+ );
+ }
+
+ _renderProperties(hasFilters) {
+ return this.props.properties.map((tooltipProperty, index) => {
/*
* Justification for dangerouslySetInnerHTML:
- * Propery value contains value generated by Field formatter
+ * Property value contains value generated by Field formatter
* Since these formatters produce raw HTML, this component needs to be able to render them as-is, relying
* on the field formatter to only produce safe HTML.
*/
-
const htmlValue = ( );
+
return (
-
- {propertyName}
- {' '}
- {htmlValue}
-
+
+
+ {tooltipProperty.getPropertyName()}
+
+
+ {htmlValue}
+
+ {this._renderFilterButton(tooltipProperty, hasFilters)}
+
);
});
}
+ _renderCloseButton() {
+ if (!this.props.showCloseButton) {
+ return null;
+ }
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+
render() {
return (
-
-
-
-
-
-
-
-
+ {this._renderCloseButton()}
- {this._renderProperties()}
+
+ {this._renderProperties()}
+
diff --git a/x-pack/plugins/maps/public/components/map/feature_tooltip.test.js b/x-pack/plugins/maps/public/components/map/feature_tooltip.test.js
new file mode 100644
index 00000000000000..324798fa1c2d8b
--- /dev/null
+++ b/x-pack/plugins/maps/public/components/map/feature_tooltip.test.js
@@ -0,0 +1,101 @@
+/*
+ * 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 { shallowWithIntl } from 'test_utils/enzyme_helpers';
+import { FeatureTooltip } from './feature_tooltip';
+
+class MockTooltipProperty {
+ constructor(key, value, isFilterable) {
+ this._key = key;
+ this._value = value;
+ this._isFilterable = isFilterable;
+ }
+
+ isFilterable() {
+ return this._isFilterable;
+ }
+
+ getFilterAction() {
+ return () => {};
+ }
+
+ getHtmlDisplayValue() {
+ return this._value;
+ }
+
+ getPropertyName() {
+ return this._key;
+ }
+}
+
+const defaultProps = {
+ properties: [],
+ closeTooltip: () => {},
+ showFilterButtons: false,
+ showCloseButton: false
+};
+
+
+const mockTooltipProperties = [
+ new MockTooltipProperty('foo', 'bar', true),
+ new MockTooltipProperty('foo', 'bar', false)
+];
+
+describe('FeatureTooltip', () => {
+
+ test('should not show close button and not show filter button', () => {
+ const component = shallowWithIntl(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('should show close button, but not filter button', () => {
+ const component = shallowWithIntl(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('should show only filter button for filterable properties', () => {
+ const component = shallowWithIntl(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('should show both filter buttons and close button', () => {
+ const component = shallowWithIntl(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+
+});
diff --git a/x-pack/plugins/maps/public/components/map/mb/index.js b/x-pack/plugins/maps/public/components/map/mb/index.js
index 7e57eaa7ee3360..b356eb015f4bfd 100644
--- a/x-pack/plugins/maps/public/components/map/mb/index.js
+++ b/x-pack/plugins/maps/public/components/map/mb/index.js
@@ -16,10 +16,12 @@ import {
setTooltipState
} from '../../../actions/store_actions';
import { getTooltipState, getLayerList, getMapReady, getGoto } from '../../../selectors/map_selectors';
+import { getIsFilterable } from '../../../store/ui';
import { getInspectorAdapters } from '../../../store/non_serializable_instances';
function mapStateToProps(state = {}) {
return {
+ isFilterable: getIsFilterable(state),
isMapReady: getMapReady(state),
layerList: getLayerList(state),
goto: getGoto(state),
@@ -53,7 +55,6 @@ function mapDispatchToProps(dispatch) {
setTooltipState(tooltipState) {
dispatch(setTooltipState(tooltipState));
}
-
};
}
diff --git a/x-pack/plugins/maps/public/components/map/mb/view.js b/x-pack/plugins/maps/public/components/map/mb/view.js
index 53d6473cbc69bc..0e129ac6f9aee9 100644
--- a/x-pack/plugins/maps/public/components/map/mb/view.js
+++ b/x-pack/plugins/maps/public/components/map/mb/view.js
@@ -62,7 +62,6 @@ export class MBMapContainer extends React.Component {
featureId: targetFeature.properties[FEATURE_ID_PROPERTY_NAME],
location: popupAnchorLocation
});
-
};
_updateHoverTooltipState = _.debounce((e) => {
@@ -251,7 +250,15 @@ export class MBMapContainer extends React.Component {
if (!this._isMounted) {
return;
}
- ReactDOM.render((), this._tooltipContainer);
+ const isLocked = this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED;
+ ReactDOM.render((
+
+ ), this._tooltipContainer);
this._mbPopup.setLngLat(location)
.setDOMContent(this._tooltipContainer)
@@ -270,8 +277,10 @@ export class MBMapContainer extends React.Component {
_syncTooltipState() {
if (this.props.tooltipState) {
+ this._mbMap.getCanvas().style.cursor = 'pointer';
this._showTooltip();
} else {
+ this._mbMap.getCanvas().style.cursor = '';
this._hideTooltip();
}
}
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.js b/x-pack/plugins/maps/public/embeddable/map_embeddable.js
index e3d9c52ab47408..5227b53f052731 100644
--- a/x-pack/plugins/maps/public/embeddable/map_embeddable.js
+++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.js
@@ -20,9 +20,9 @@ import {
setGotoWithCenter,
replaceLayerList,
setQuery,
- setRefreshConfig,
+ setRefreshConfig
} from '../actions/store_actions';
-import { setReadOnly } from '../store/ui';
+import { setReadOnly, setFilterable } from '../store/ui';
import { getInspectorAdapters } from '../store/non_serializable_instances';
import { getMapCenter, getMapZoom } from '../selectors/map_selectors';
@@ -82,6 +82,7 @@ export class MapEmbeddable extends Embeddable {
*/
render(domNode, containerState) {
this._store.dispatch(setReadOnly(true));
+ this._store.dispatch(setFilterable(true));
if (this._embeddableConfig.mapCenter) {
this._store.dispatch(setGotoWithCenter({
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js
index 8ab8efde42d97b..50e5ce35d6eb69 100644
--- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js
+++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.js
@@ -10,7 +10,7 @@ import { EmbeddableFactory } from 'ui/embeddable';
import { MapEmbeddable } from './map_embeddable';
import { indexPatternService } from '../kibana_services';
import { i18n } from '@kbn/i18n';
-import { createMapPath, MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
+import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants';
export class MapEmbeddableFactory extends EmbeddableFactory {
@@ -22,7 +22,7 @@ export class MapEmbeddableFactory extends EmbeddableFactory {
defaultMessage: 'Map',
}),
type: MAP_SAVED_OBJECT_TYPE,
- getIconForSavedObject: () => 'gisApp',
+ getIconForSavedObject: () => APP_ICON
},
});
this._savedObjectLoader = gisMapSavedObjectLoader;
diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js
index d8d395ef414b12..6c80774383b2bd 100644
--- a/x-pack/plugins/maps/public/kibana_services.js
+++ b/x-pack/plugins/maps/public/kibana_services.js
@@ -6,12 +6,12 @@
import { uiModules } from 'ui/modules';
import { SearchSourceProvider } from 'ui/courier';
-import { timefilter } from 'ui/timefilter/timefilter';
+import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils';
-export const timeService = timefilter;
export let indexPatternService;
export let SearchSource;
+export let filterBarQueryFilter;
export async function fetchSearchSourceAndRecordWithInspector({ searchSource, requestId, requestName, requestDesc, inspectorAdapters }) {
const inspectorRequest = inspectorAdapters.requests.start(
@@ -39,4 +39,5 @@ uiModules.get('app/maps').run(($injector) => {
indexPatternService = $injector.get('indexPatterns');
const Private = $injector.get('Private');
SearchSource = Private(SearchSourceProvider);
+ filterBarQueryFilter = Private(FilterBarQueryFilterProvider);
});
diff --git a/x-pack/plugins/maps/public/shared/layers/joins/left_inner_join.js b/x-pack/plugins/maps/public/shared/layers/joins/left_inner_join.js
index d64b4806d01235..70346123352916 100644
--- a/x-pack/plugins/maps/public/shared/layers/joins/left_inner_join.js
+++ b/x-pack/plugins/maps/public/shared/layers/joins/left_inner_join.js
@@ -67,7 +67,7 @@ export class LeftInnerJoin {
return { ...featureCollection };
}
- getJoinSource() {
+ getRightJoinSource() {
return this._rightSource;
}
diff --git a/x-pack/plugins/maps/public/shared/layers/sources/es_join_source.js b/x-pack/plugins/maps/public/shared/layers/sources/es_join_source.js
index de911fdb8dda40..c141ab28e0f541 100644
--- a/x-pack/plugins/maps/public/shared/layers/sources/es_join_source.js
+++ b/x-pack/plugins/maps/public/shared/layers/sources/es_join_source.js
@@ -10,6 +10,7 @@ import { AbstractESSource } from './es_source';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { AggConfigs } from 'ui/vis/agg_configs';
import { i18n } from '@kbn/i18n';
+import { ESTooltipProperty } from '../tooltips/es_tooltip_property';
const TERMS_AGG_NAME = 'join';
@@ -74,6 +75,10 @@ export class ESJoinSource extends AbstractESSource {
return [this._descriptor.indexPatternId];
}
+ getTerm() {
+ return this._descriptor.term;
+ }
+
_formatMetricKey(metric) {
const metricKey = metric.type !== 'count' ? `${metric.type}_of_${metric.field}` : metric.type;
return `__kbnjoin__${metricKey}_groupby_${this._descriptor.indexPatternTitle}.${this._descriptor.term}`;
@@ -180,4 +185,16 @@ export class ESJoinSource extends AbstractESSource {
async filterAndFormatPropertiesToHtml(properties) {
return await this.filterAndFormatPropertiesToHtmlForMetricFields(properties);
}
+
+ async createESTooltipProperty(propertyName, rawValue) {
+ try {
+ const indexPattern = await this._getIndexPattern();
+ if (!indexPattern) {
+ return null;
+ }
+ return new ESTooltipProperty(propertyName, rawValue, indexPattern);
+ } catch (e) {
+ return null;
+ }
+ }
}
diff --git a/x-pack/plugins/maps/public/shared/layers/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/shared/layers/sources/es_search_source/es_search_source.js
index e49520fdb029d7..03d598c029de9c 100644
--- a/x-pack/plugins/maps/public/shared/layers/sources/es_search_source/es_search_source.js
+++ b/x-pack/plugins/maps/public/shared/layers/sources/es_search_source/es_search_source.js
@@ -15,6 +15,8 @@ import { UpdateSourceEditor } from './update_source_editor';
import { ES_SEARCH } from '../../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../../common/i18n_getters';
+import { ESTooltipProperty } from '../../tooltips/es_tooltip_property';
+
import { DEFAULT_ES_DOC_LIMIT, DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants';
export class ESSearchSource extends AbstractESSource {
@@ -167,30 +169,19 @@ export class ESSearchSource extends AbstractESSource {
}
async filterAndFormatPropertiesToHtml(properties) {
- const filteredProperties = {};
- this._descriptor.tooltipProperties.forEach(propertyName => {
- filteredProperties[propertyName] = _.get(properties, propertyName, '-');
- });
-
+ const tooltipProps = [];
let indexPattern;
try {
indexPattern = await this._getIndexPattern();
} catch(error) {
console.warn(`Unable to find Index pattern ${this._descriptor.indexPatternId}, values are not formatted`);
- return filteredProperties;
+ return [];
}
this._descriptor.tooltipProperties.forEach(propertyName => {
- const field = indexPattern.fields.byName[propertyName];
- if (!field) {
- return;
- }
- const htmlConverter = field.format.getConverterFor('html');
- filteredProperties[propertyName] = (htmlConverter) ? htmlConverter(filteredProperties[propertyName]) :
- field.format.convert(filteredProperties[propertyName]);
+ tooltipProps.push(new ESTooltipProperty(propertyName, properties[propertyName], indexPattern));
});
-
- return filteredProperties;
+ return tooltipProps;
}
isFilterByMapBounds() {
diff --git a/x-pack/plugins/maps/public/shared/layers/sources/es_source.js b/x-pack/plugins/maps/public/shared/layers/sources/es_source.js
index 54a83360fea3a8..c348d22c33fb0c 100644
--- a/x-pack/plugins/maps/public/shared/layers/sources/es_source.js
+++ b/x-pack/plugins/maps/public/shared/layers/sources/es_source.js
@@ -15,6 +15,8 @@ import { timefilter } from 'ui/timefilter/timefilter';
import _ from 'lodash';
import { AggConfigs } from 'ui/vis/agg_configs';
import { i18n } from '@kbn/i18n';
+import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property';
+
import uuid from 'uuid/v4';
import { copyPersistentState } from '../../../store/util';
@@ -99,35 +101,24 @@ export class AbstractESSource extends AbstractVectorSource {
return properties;
}
- function formatMetricValue(metricField, propertyValue) {
- if (metricField.type === 'count') {
- return propertyValue;
- }
-
- const indexPatternField = indexPattern.fields.byName[metricField.field];
- if (!indexPatternField) {
- return propertyValue;
- }
-
- const htmlConverter = indexPatternField.format.getConverterFor('html');
- return (htmlConverter)
- ? htmlConverter(propertyValue)
- : indexPatternField.format.convert(propertyValue);
- }
const metricFields = this.getMetricFields();
- const tooltipProps = {};
+ const tooltipProperties = [];
metricFields.forEach((metricField) => {
let value;
for (const key in properties) {
if (properties.hasOwnProperty(key) && metricField.propertyKey === key) {
- value = formatMetricValue(metricField, properties[key]);
+ value = properties[key];
break;
}
}
- tooltipProps[metricField.propertyLabel] = (typeof value === 'undefined') ? '-' : value;
+
+ const tooltipProperty = new ESAggMetricTooltipProperty(metricField.propertyLabel, value, indexPattern, metricField);
+ tooltipProperties.push(tooltipProperty);
});
- return tooltipProps;
+
+ return tooltipProperties;
+
}
diff --git a/x-pack/plugins/maps/public/shared/layers/sources/vector_source.js b/x-pack/plugins/maps/public/shared/layers/sources/vector_source.js
index 5b2467a9da23ef..7c1e8f14072f7b 100644
--- a/x-pack/plugins/maps/public/shared/layers/sources/vector_source.js
+++ b/x-pack/plugins/maps/public/shared/layers/sources/vector_source.js
@@ -6,6 +6,7 @@
import { VectorLayer } from '../vector_layer';
+import { TooltipProperty } from '../tooltips/tooltip_property';
import { VectorStyle } from '../styles/vector_style';
import { AbstractSource } from './source';
import * as topojson from 'topojson-client';
@@ -96,15 +97,14 @@ export class AbstractVectorSource extends AbstractSource {
// Allow source to filter and format feature properties before displaying to user
async filterAndFormatPropertiesToHtml(properties) {
- //todo :this is quick hack... should revise (should model proeprties explicitly in vector_layer
- const props = {};
+ const tooltipProperties = [];
for (const key in properties) {
if (key.startsWith('__kbn')) {//these are system properties and should be ignored
continue;
}
- props[key] = _.escape(properties[key]);
+ tooltipProperties.push(new TooltipProperty(key, properties[key]));
}
- return props;
+ return tooltipProperties;
}
async isTimeAware() {
diff --git a/x-pack/plugins/maps/public/shared/layers/tooltips/es_aggmetric_tooltip_property.js b/x-pack/plugins/maps/public/shared/layers/tooltips/es_aggmetric_tooltip_property.js
new file mode 100644
index 00000000000000..ba8131f349542f
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/tooltips/es_aggmetric_tooltip_property.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+import { ESTooltipProperty } from './es_tooltip_property';
+
+export class ESAggMetricTooltipProperty extends ESTooltipProperty {
+
+ constructor(propertyName, rawValue, indexPattern, metricField) {
+ super(propertyName, rawValue, indexPattern);
+ this._metricField = metricField;
+ }
+ isFilterable() {
+ return false;
+ }
+
+ getHtmlDisplayValue() {
+ if (typeof this._rawValue === 'undefined') {
+ return '-';
+ }
+ if (this._metricField.type === 'count') {
+ return this._rawValue;
+ }
+ const indexPatternField = this._indexPattern.fields.byName[this._metricField.field];
+ if (!indexPatternField) {
+ return this._rawValue;
+ }
+ const htmlConverter = indexPatternField.format.getConverterFor('html');
+
+ return (htmlConverter)
+ ? htmlConverter(this._rawValue)
+ : indexPatternField.format.convert(this._rawValue);
+
+ }
+
+}
diff --git a/x-pack/plugins/maps/public/shared/layers/tooltips/es_tooltip_property.js b/x-pack/plugins/maps/public/shared/layers/tooltips/es_tooltip_property.js
new file mode 100644
index 00000000000000..f3df3a198c0b0d
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/tooltips/es_tooltip_property.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { buildPhraseFilter } from '@kbn/es-query';
+import { filterBarQueryFilter } from '../../../kibana_services';
+import { TooltipProperty } from './tooltip_property';
+import _ from 'lodash';
+
+export class ESTooltipProperty extends TooltipProperty {
+
+ constructor(propertyName, rawValue, indexPattern) {
+ super(propertyName, rawValue);
+ this._indexPattern = indexPattern;
+ }
+
+ getHtmlDisplayValue() {
+
+ if (typeof this._rawValue === 'undefined') {
+ return '-';
+ }
+
+ const field = this._indexPattern.fields.byName[this._propertyName];
+ if (!field) {
+ return _.escape(this._rawValue);
+ }
+ const htmlConverter = field.format.getConverterFor('html');
+ return htmlConverter ? htmlConverter(this._rawValue) : field.format.convert(this._rawValue);
+ }
+
+ isFilterable() {
+ const field = this._indexPattern.fields.byName[this._propertyName];
+ return field && (field.type === 'string' || field.type === 'date' || field.type === 'ip' || field.type === 'number');
+ }
+
+ getESFilter() {
+ return buildPhraseFilter(
+ this._indexPattern.fields.byName[this._propertyName],
+ this._rawValue,
+ this._indexPattern);
+ }
+
+ getFilterAction() {
+ return () => {
+ const phraseFilter = this.getESFilter();
+ filterBarQueryFilter.addFilters([phraseFilter]);
+ };
+ }
+}
diff --git a/x-pack/plugins/maps/public/shared/layers/tooltips/join_tooltip_property.js b/x-pack/plugins/maps/public/shared/layers/tooltips/join_tooltip_property.js
new file mode 100644
index 00000000000000..db48c94c1616df
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/tooltips/join_tooltip_property.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+import { filterBarQueryFilter } from '../../../kibana_services';
+import { TooltipProperty } from './tooltip_property';
+
+export class JoinTooltipProperty extends TooltipProperty {
+
+ constructor(tooltipProperty, leftInnerJoins) {
+ super();
+ this._tooltipProperty = tooltipProperty;
+ this._leftInnerJoins = leftInnerJoins;
+ }
+
+ isFilterable() {
+ return true;
+ }
+
+ getPropertyName() {
+ return this._tooltipProperty.getPropertyName();
+ }
+
+ getHtmlDisplayValue() {
+ return this._tooltipProperty.getHtmlDisplayValue();
+ }
+
+ getFilterAction() {
+ //dispatch all the filter actions to the query bar
+ //this relies on the de-duping of filterBarQueryFilter
+ return async () => {
+ const esFilters = [];
+ if (this._tooltipProperty.isFilterable()) {
+ esFilters.push(this._tooltipProperty.getESFilter());
+ }
+
+ for (let i = 0; i < this._leftInnerJoins.length; i++) {
+ const rightSource = this._leftInnerJoins[i].getRightJoinSource();
+ const esTooltipProperty = await rightSource.createESTooltipProperty(
+ rightSource.getTerm(),
+ this._tooltipProperty.getRawValue()
+ );
+ if (esTooltipProperty) {
+ const filter = esTooltipProperty.getESFilter();
+ esFilters.push(filter);
+ }
+ }
+ filterBarQueryFilter.addFilters(esFilters);
+ };
+ }
+
+
+}
diff --git a/x-pack/plugins/maps/public/shared/layers/tooltips/tooltip_property.js b/x-pack/plugins/maps/public/shared/layers/tooltips/tooltip_property.js
new file mode 100644
index 00000000000000..3f3a9ca2829bfa
--- /dev/null
+++ b/x-pack/plugins/maps/public/shared/layers/tooltips/tooltip_property.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import _ from 'lodash';
+
+export class TooltipProperty {
+
+ constructor(propertyName, rawValue) {
+ this._propertyName = propertyName;
+ this._rawValue = rawValue;
+ }
+
+ getPropertyName() {
+ return this._propertyName;
+ }
+
+ getHtmlDisplayValue() {
+ return _.escape(this._rawValue);
+ }
+
+ getRawValue() {
+ return this._rawValue;
+ }
+
+ isFilterable() {
+ return false;
+ }
+
+ getFilterAction() {
+ throw new Error('This property is not filterable');
+ }
+}
diff --git a/x-pack/plugins/maps/public/shared/layers/vector_layer.js b/x-pack/plugins/maps/public/shared/layers/vector_layer.js
index abd9e02e6a36d9..f96ccda3d54578 100644
--- a/x-pack/plugins/maps/public/shared/layers/vector_layer.js
+++ b/x-pack/plugins/maps/public/shared/layers/vector_layer.js
@@ -10,6 +10,7 @@ import { VectorStyle } from './styles/vector_style';
import { LeftInnerJoin } from './joins/left_inner_join';
import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN } from '../../../common/constants';
import _ from 'lodash';
+import { JoinTooltipProperty } from './tooltips/join_tooltip_property';
const EMPTY_FEATURE_COLLECTION = {
type: 'FeatureCollection',
@@ -159,6 +160,7 @@ export class VectorLayer extends AbstractLayer {
}
async _canSkipSourceUpdate(source, sourceDataId, searchFilters) {
+
const timeAware = await source.isTimeAware();
const refreshTimerAware = await source.isRefreshTimerAware();
const extentAware = source.isFilterByMapBounds();
@@ -230,7 +232,7 @@ export class VectorLayer extends AbstractLayer {
async _syncJoin({ join, startLoading, stopLoading, onLoadError, dataFilters }) {
- const joinSource = join.getJoinSource();
+ const joinSource = join.getRightJoinSource();
const sourceDataId = join.getSourceId();
const requestToken = Symbol(`layer-join-refresh:${ this.getId()} - ${sourceDataId}`);
@@ -517,13 +519,34 @@ export class VectorLayer extends AbstractLayer {
return [this._getMbPointLayerId(), this._getMbLineLayerId(), this._getMbPolygonLayerId()];
}
+
+ _addJoinsToSourceTooltips(tooltipsFromSource) {
+ for (let i = 0; i < tooltipsFromSource.length; i++) {
+ const tooltipProperty = tooltipsFromSource[i];
+ const matchingJoins = [];
+ for (let j = 0; j < this._joins.length; j++) {
+ if (this._joins[j].getLeftFieldName() === tooltipProperty.getPropertyName()) {
+ matchingJoins.push(this._joins[j]);
+ }
+ }
+ if (matchingJoins.length) {
+ tooltipsFromSource[i] = new JoinTooltipProperty(tooltipProperty, matchingJoins);
+ }
+ }
+ }
+
+
async getPropertiesForTooltip(properties) {
- const tooltipsFromSource = await this._source.filterAndFormatPropertiesToHtml(properties);
+
+ let allTooltips = await this._source.filterAndFormatPropertiesToHtml(properties);
+ this._addJoinsToSourceTooltips(allTooltips);
+
+
for (let i = 0; i < this._joins.length; i++) {
const propsFromJoin = await this._joins[i].filterAndFormatPropertiesForTooltip(properties);
- Object.assign(tooltipsFromSource, propsFromJoin);
+ allTooltips = [...allTooltips, ...propsFromJoin];
}
- return tooltipsFromSource;
+ return allTooltips;
}
canShowTooltip() {
diff --git a/x-pack/plugins/maps/public/store/map.js b/x-pack/plugins/maps/public/store/map.js
index 5b5f564b333f46..6f4aae3660d4a3 100644
--- a/x-pack/plugins/maps/public/store/map.js
+++ b/x-pack/plugins/maps/public/store/map.js
@@ -166,7 +166,7 @@ export function map(state = INITIAL_STATE, action) {
...layerList.slice(0, layerIdx),
{
...layerList[layerIdx],
- __isInErrorState: true,
+ __isInErrorState: action.isInErrorState,
__errorMessage: action.errorMessage
},
...layerList.slice(layerIdx + 1)
diff --git a/x-pack/plugins/maps/public/store/ui.js b/x-pack/plugins/maps/public/store/ui.js
index 7804fe26b6fe58..6334d49b0a3201 100644
--- a/x-pack/plugins/maps/public/store/ui.js
+++ b/x-pack/plugins/maps/public/store/ui.js
@@ -3,13 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
-
export const UPDATE_FLYOUT = 'UPDATE_FLYOUT';
export const CLOSE_SET_VIEW = 'CLOSE_SET_VIEW';
export const OPEN_SET_VIEW = 'OPEN_SET_VIEW';
export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
export const SET_READ_ONLY = 'SET_READ_ONLY';
+export const SET_FILTERABLE = 'IS_FILTERABLE';
export const FLYOUT_STATE = {
NONE: 'NONE',
LAYER_PANEL: 'LAYER_PANEL',
@@ -20,6 +19,7 @@ const INITIAL_STATE = {
flyoutDisplay: FLYOUT_STATE.NONE,
isFullScreen: false,
isReadOnly: false,
+ isFilterable: false
};
// Reducer
@@ -35,6 +35,8 @@ export function ui(state = INITIAL_STATE, action) {
return { ...state, isFullScreen: action.isFullScreen };
case SET_READ_ONLY:
return { ...state, isReadOnly: action.isReadOnly };
+ case SET_FILTERABLE:
+ return { ...state, isFilterable: action.isFilterable };
default:
return state;
}
@@ -76,9 +78,17 @@ export function setReadOnly(isReadOnly) {
};
}
+export function setFilterable(isFilterable) {
+ return {
+ type: SET_FILTERABLE,
+ isFilterable
+ };
+}
+
// Selectors
export const getFlyoutDisplay = ({ ui }) => ui && ui.flyoutDisplay
|| INITIAL_STATE.flyoutDisplay;
-export const getIsSetViewOpen = ({ ui }) => _.get(ui, 'isSetViewOpen', false);
-export const getIsFullScreen = ({ ui }) => _.get(ui, 'isFullScreen', false);
-export const getIsReadOnly = ({ ui }) => _.get(ui, 'isReadOnly', true);
+export const getIsSetViewOpen = ({ ui }) => ui.isSetViewOpen;
+export const getIsFullScreen = ({ ui }) => ui.isFullScreen;
+export const getIsReadOnly = ({ ui }) => ui.isReadOnly;
+export const getIsFilterable = ({ ui }) => ui.isFilterable;
diff --git a/x-pack/plugins/ml/common/constants/annotations.ts b/x-pack/plugins/ml/common/constants/annotations.ts
index c388df553df7e0..936ff610361afd 100644
--- a/x-pack/plugins/ml/common/constants/annotations.ts
+++ b/x-pack/plugins/ml/common/constants/annotations.ts
@@ -10,3 +10,6 @@ export enum ANNOTATION_TYPE {
}
export const ANNOTATION_USER_UNKNOWN = '';
+
+// UI enforced limit to the maximum number of characters that can be entered for an annotation.
+export const ANNOTATION_MAX_LENGTH_CHARS = 1000;
diff --git a/x-pack/plugins/ml/public/components/annotations/annotation_flyout/index.test.tsx b/x-pack/plugins/ml/public/components/annotations/annotation_flyout/index.test.tsx
index 96b1d3e9bdb191..871dcd74d0907e 100644
--- a/x-pack/plugins/ml/public/components/annotations/annotation_flyout/index.test.tsx
+++ b/x-pack/plugins/ml/public/components/annotations/annotation_flyout/index.test.tsx
@@ -4,8 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
-import { shallowWithIntl } from 'test_utils/enzyme_helpers';
+import { injectObservablesAsProps } from '../../../util/observable_utils';
+import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json';
+
+import React, { ComponentType } from 'react';
+import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
+
+import { Annotation } from '../../../../common/types/annotations';
+import { annotation$ } from '../../../services/annotations_service';
import { AnnotationFlyout } from './index';
@@ -14,4 +20,36 @@ describe('AnnotationFlyout', () => {
const wrapper = shallowWithIntl( );
expect(wrapper).toMatchSnapshot();
});
+
+ test('Update button is disabled with empty annotation', () => {
+ const annotation = mockAnnotations[1] as Annotation;
+ annotation$.next(annotation);
+
+ // injectObservablesAsProps wraps the observable in a new component
+ const ObservableComponent = injectObservablesAsProps(
+ { annotation: annotation$ },
+ (AnnotationFlyout as any) as ComponentType
+ );
+
+ const wrapper = mountWithIntl( );
+ const updateBtn = wrapper.find('EuiButton').first();
+ expect(updateBtn.prop('isDisabled')).toEqual(true);
+ });
+
+ test('Error displayed and update button displayed if annotation text is longer than max chars', () => {
+ const annotation = mockAnnotations[2] as Annotation;
+ annotation$.next(annotation);
+
+ // injectObservablesAsProps wraps the observable in a new component
+ const ObservableComponent = injectObservablesAsProps(
+ { annotation: annotation$ },
+ (AnnotationFlyout as any) as ComponentType
+ );
+
+ const wrapper = mountWithIntl( );
+ const updateBtn = wrapper.find('EuiButton').first();
+ expect(updateBtn.prop('isDisabled')).toEqual(true);
+
+ expect(wrapper.find('EuiFormErrorText')).toHaveLength(1);
+ });
});
diff --git a/x-pack/plugins/ml/public/components/annotations/annotation_flyout/index.tsx b/x-pack/plugins/ml/public/components/annotations/annotation_flyout/index.tsx
index 9b53adb7233253..5086b29a857250 100644
--- a/x-pack/plugins/ml/public/components/annotations/annotation_flyout/index.tsx
+++ b/x-pack/plugins/ml/public/components/annotations/annotation_flyout/index.tsx
@@ -26,6 +26,7 @@ import { CommonProps } from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { InjectedIntlProps } from 'react-intl';
import { toastNotifications } from 'ui/notify';
+import { ANNOTATION_MAX_LENGTH_CHARS } from '../../../../common/constants/annotations';
import {
annotation$,
annotationsRefresh$,
@@ -112,6 +113,45 @@ class AnnotationFlyoutIntl extends Component {
+ // Validates the entered text, returning an array of error messages
+ // for display in the form. An empty array is returned if the text is valid.
+ const { annotation, intl } = this.props;
+ const errors: string[] = [];
+ if (annotation === null) {
+ return errors;
+ }
+
+ if (annotation.annotation.trim().length === 0) {
+ errors.push(
+ intl.formatMessage({
+ id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError',
+ defaultMessage: 'Enter annotation text',
+ })
+ );
+ }
+
+ const textLength = annotation.annotation.length;
+ if (textLength > ANNOTATION_MAX_LENGTH_CHARS) {
+ const charsOver = textLength - ANNOTATION_MAX_LENGTH_CHARS;
+ errors.push(
+ intl.formatMessage(
+ {
+ id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.maxLengthError',
+ defaultMessage:
+ '{charsOver, number} {charsOver, plural, one {character} other {characters}} above maximum length of {maxChars}',
+ },
+ {
+ maxChars: ANNOTATION_MAX_LENGTH_CHARS,
+ charsOver,
+ }
+ )
+ );
+ }
+
+ return errors;
+ };
+
public saveOrUpdateAnnotation = () => {
const { annotation, intl } = this.props;
@@ -179,7 +219,7 @@ class AnnotationFlyoutIntl extends Component 0;
+ const lengthRatioToShowWarning = 0.95;
+ let helpText = null;
+ if (
+ isInvalid === false &&
+ annotation.annotation.length > ANNOTATION_MAX_LENGTH_CHARS * lengthRatioToShowWarning
+ ) {
+ helpText = intl.formatMessage(
+ {
+ id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.approachingMaxLengthWarning',
+ defaultMessage:
+ '{charsRemaining, number} {charsRemaining, plural, one {character} other {characters}} remaining',
+ },
+ { charsRemaining: ANNOTATION_MAX_LENGTH_CHARS - annotation.annotation.length }
+ );
+ }
+
return (
@@ -219,10 +279,13 @@ class AnnotationFlyoutIntl extends Component
}
fullWidth
+ helpText={helpText}
+ isInvalid={isInvalid}
+ error={validationErrors}
>
{isExistingAnnotation ? (
diff --git a/x-pack/plugins/ml/public/components/annotations/annotations_table/__mocks__/mock_annotations.json b/x-pack/plugins/ml/public/components/annotations/annotations_table/__mocks__/mock_annotations.json
index 2964989316da09..06ed1937626c09 100644
--- a/x-pack/plugins/ml/public/components/annotations/annotations_table/__mocks__/mock_annotations.json
+++ b/x-pack/plugins/ml/public/components/annotations/annotations_table/__mocks__/mock_annotations.json
@@ -10,5 +10,30 @@
"modified_time": 1546417097181,
"modified_username": "",
"_id": "KCCkDWgB_ZdQ1MFDSYPi"
+ },
+ {
+ "timestamp": 1455026177994,
+ "end_timestamp": 1455041968976,
+ "annotation": "",
+ "job_id": "farequote",
+ "type": "annotation",
+ "create_time": 1554377048000,
+ "create_username": "sysadmin",
+ "modified_time": 1554377048000,
+ "modified_username": "sysadmin",
+ "_id": "KCCkDWgB_ZdQ1MFDSYPj"
+ },
+ {
+ "timestamp": 1455026177994,
+ "end_timestamp": 1455041968976,
+ "annotation":
+ "A very long annotation with more than the maximum allowed characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
+ "job_id": "farequote",
+ "type": "annotation",
+ "create_time": 1554377253000,
+ "create_username": "sysadmin",
+ "modified_time": 1554377253000,
+ "modified_username": "sysadmin",
+ "_id": "KCCkDWgB_ZdQ1MFDSYPk"
}
]
diff --git a/x-pack/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.js b/x-pack/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.js
index f2633e4fbb7a97..9ec775db4e2b29 100644
--- a/x-pack/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.js
+++ b/x-pack/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.js
@@ -43,7 +43,7 @@ describe('AnnotationsTable', () => {
});
test('Initialization with annotations prop.', () => {
- const wrapper = shallowWithIntl( );
+ const wrapper = shallowWithIntl( );
expect(wrapper).toMatchSnapshot();
});
diff --git a/x-pack/plugins/ml/public/file_datavisualizer/components/import_view/import_view.js b/x-pack/plugins/ml/public/file_datavisualizer/components/import_view/import_view.js
index fde8bd0c4afdf2..bbdcb5d4db4fc9 100644
--- a/x-pack/plugins/ml/public/file_datavisualizer/components/import_view/import_view.js
+++ b/x-pack/plugins/ml/public/file_datavisualizer/components/import_view/import_view.js
@@ -641,10 +641,10 @@ function isIndexPatternNameValid(name, indexPatternNames, index) {
}
// escape . and + to stop the regex matching more than it should.
- let newName = name.replace('.', '\\.');
- newName = newName.replace('+', '\\+');
+ let newName = name.replace(/\./g, '\\.');
+ newName = newName.replace(/\+/g, '\\+');
// replace * with .* to make the wildcard match work.
- newName = newName.replace('*', '.*');
+ newName = newName.replace(/\*/g, '.*');
const reg = new RegExp(`^${newName}$`);
if (index.match(reg) === null) { // name should match index
return (
diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js
index 7fe1a004b251c0..4d0fb34294ad16 100644
--- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js
+++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_controller.js
@@ -420,7 +420,8 @@ module.controller('MlTimeSeriesExplorerController', function (
refreshFocusData.focusChartData,
refreshFocusData.anomalyRecords,
$scope.timeFieldName,
- $scope.focusAggregationInterval);
+ $scope.focusAggregationInterval,
+ $scope.modelPlotEnabled);
refreshFocusData.focusChartData = processScheduledEventsForChart(
refreshFocusData.focusChartData,
diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js
index 7d76b4a242ec67..33048cb0f9d099 100644
--- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js
+++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js
@@ -98,7 +98,8 @@ export function processDataForFocusAnomalies(
chartData,
anomalyRecords,
timeFieldName,
- aggregationInterval) {
+ aggregationInterval,
+ modelPlotEnabled) {
const timesToAddPointsFor = [];
@@ -122,10 +123,16 @@ export function processDataForFocusAnomalies(
timesToAddPointsFor.sort((a, b) => a - b);
timesToAddPointsFor.forEach((time) => {
- chartData.push({
+ const pointToAdd = {
date: new Date(time),
value: null
- });
+ };
+
+ if (modelPlotEnabled === true) {
+ pointToAdd.upper = null;
+ pointToAdd.lower = null;
+ }
+ chartData.push(pointToAdd);
});
// Iterate through the anomaly records adding the
@@ -257,10 +264,9 @@ export function findChartPointForAnomalyTime(chartData, anomalyTime, aggregation
// which contains the anomalous bucket.
let foundItem;
const intervalMs = aggregationInterval.asMilliseconds();
- const anomalyTimeForAggInt = (Math.floor(anomalyTime / intervalMs)) * intervalMs;
for (let i = 0; i < chartData.length; i++) {
const itemTime = chartData[i].date.getTime();
- if (itemTime === anomalyTimeForAggInt) {
+ if (anomalyTime - itemTime < intervalMs) {
foundItem = chartData[i];
break;
}
diff --git a/x-pack/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html
index 28870832c0b1d4..5ea7ad1d05df14 100644
--- a/x-pack/plugins/monitoring/public/directives/main/index.html
+++ b/x-pack/plugins/monitoring/public/directives/main/index.html
@@ -204,25 +204,11 @@
i18n-default-message="Advanced"
>
-
-
{{ monitoringMain.pipelineId }}
-
-
-
-
+
diff --git a/x-pack/plugins/monitoring/public/directives/main/index.js b/x-pack/plugins/monitoring/public/directives/main/index.js
index a0e3504cf5cb6e..b9af69d55c7297 100644
--- a/x-pack/plugins/monitoring/public/directives/main/index.js
+++ b/x-pack/plugins/monitoring/public/directives/main/index.js
@@ -3,13 +3,57 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
+import React from 'react';
+import { render, unmountComponentAtNode } from 'react-dom';
+import {
+ EuiSelect,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import { uiModules } from 'ui/modules';
import template from './index.html';
import { shortenPipelineHash } from '../../../common/formatting';
import 'ui/directives/kbn_href';
+const setOptions = (controller) => {
+ if (!controller.pipelineVersions || !controller.pipelineVersions.length || !controller.pipelineDropdownElement) {
+ return;
+ }
+
+ render(
+
+
+
+ {controller.pipelineId}
+
+
+
+ {
+ return {
+ text: i18n.translate('xpack.monitoring.logstashNavigation.pipelineVersionDescription',
+ {
+ defaultMessage: 'Version active {relativeLastSeen} and first seen {relativeFirstSeen}',
+ values: {
+ relativeLastSeen: option.relativeLastSeen,
+ relativeFirstSeen: option.relativeFirstSeen
+ }
+ }
+ ),
+ value: option.hash
+ };
+ })}
+ onChange={controller.onChangePipelineHash}
+ />
+
+
+ , controller.pipelineDropdownElement);
+};
+
/*
* Manage data and provide helper methods for the "main" directive's template
*/
@@ -26,6 +70,11 @@ export class MonitoringMainController {
this.inApm = false;
}
+ dropdownLoadedHandler() {
+ this.pipelineDropdownElement = document.querySelector('#dropdown-elm');
+ setOptions(this);
+ }
+
// kick things off from the directive link function
setup(options) {
this._licenseService = options.licenseService;
@@ -114,8 +163,10 @@ uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, $injector) =
Object.keys(setupObj.attributes).forEach(key => {
attributes.$observe(key, () => controller.setup(getSetupObj()));
});
+ scope.$on('$destroy', () => controller.pipelineDropdownElement && unmountComponentAtNode(controller.pipelineDropdownElement));
scope.$watch('pageData.versions', versions => {
controller.pipelineVersions = versions;
+ setOptions(controller);
});
}
};
diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js
index d0d870d2a58e2f..5dcf584436943a 100644
--- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js
+++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js
@@ -204,7 +204,10 @@ describe(' ', () => {
describe('confirmation modal (delete remote cluster)', () => {
test('should remove the remote cluster from the table after delete is successful', async () => {
// Mock HTTP DELETE request
- setDeleteRemoteClusterResponse();
+ setDeleteRemoteClusterResponse({
+ itemsDeleted: [remoteCluster1.name],
+ errors: [],
+ });
// Make sure that we have our 2 remote clusters in the table
expect(rows.length).toBe(2);
diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/test_helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/test_helpers.js
index 353eef4646dee8..52652d31d5c040 100644
--- a/x-pack/plugins/remote_clusters/__jest__/client_integration/test_helpers.js
+++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/test_helpers.js
@@ -112,7 +112,10 @@ export const registerHttpRequestMockHelpers = server => {
};
const setDeleteRemoteClusterResponse = (response) => {
- const defaultResponse = { success: true };
+ const defaultResponse = {
+ itemsDeleted: [],
+ errors: [],
+ };
server.respondWith('DELETE', /api\/remote_clusters/,
mockResponse(defaultResponse, response)
diff --git a/x-pack/plugins/remote_clusters/public/store/actions/remove_clusters.js b/x-pack/plugins/remote_clusters/public/store/actions/remove_clusters.js
index 4c17b2e2e24928..d72b6a5d1264a2 100644
--- a/x-pack/plugins/remote_clusters/public/store/actions/remove_clusters.js
+++ b/x-pack/plugins/remote_clusters/public/store/actions/remove_clusters.js
@@ -19,49 +19,75 @@ import {
import { closeDetailPanel } from './detail_panel';
import { getDetailPanelClusterName } from '../selectors';
+function getErrorTitle(count, name = null) {
+ if (count === 1) {
+ if (name) {
+ return i18n.translate('xpack.remoteClusters.removeAction.errorSingleNotificationTitle', {
+ defaultMessage: `Error removing remote cluster '{name}'`,
+ values: { name },
+ });
+ }
+ } else {
+ return i18n.translate('xpack.remoteClusters.removeAction.errorMultipleNotificationTitle', {
+ defaultMessage: `Error removing '{count}' remote clusters`,
+ values: { count },
+ });
+ }
+}
+
export const removeClusters = (names) => async (dispatch, getState) => {
dispatch({
type: REMOVE_CLUSTERS_START,
});
- const removalSuccesses = [];
- const removalErrors = [];
- const removeClusterRequests = names.map(name => {
- sendRemoveClusterRequest(name)
- .then(() => removalSuccesses.push(name))
- .catch(() => removalErrors.push(name));
- });
+ let itemsDeleted = [];
+ let errors = [];
await Promise.all([
- ...removeClusterRequests,
- // Wait at least half a second to avoid a weird flicker of the saving feedback.
+ sendRemoveClusterRequest(names.join(','))
+ .then((response) => {
+ ({ itemsDeleted, errors } = response.data);
+ }),
+ // Wait at least half a second to avoid a weird flicker of the saving feedback (only visible
+ // when requests resolve very quickly).
new Promise(resolve => setTimeout(resolve, 500)),
- ]);
+ ]).catch(error => {
+ const errorTitle = getErrorTitle(names.length, names[0]);
+ toastNotifications.addDanger({
+ title: errorTitle,
+ text: error.data.message,
+ });
+ });
- if(removalErrors.length > 0) {
- if (removalErrors.length === 1) {
- toastNotifications.addDanger(i18n.translate('xpack.remoteClusters.removeAction.errorSingleNotificationTitle', {
- defaultMessage: `Error removing remote cluster '{name}'`,
- values: { name: removalErrors[0] },
- }));
- } else {
- toastNotifications.addDanger(i18n.translate('xpack.remoteClusters.removeAction.errorMultipleNotificationTitle', {
- defaultMessage: `Error removing '{count}' remote clusters`,
- values: { count: removalErrors.length },
- }));
- }
+ if (errors.length > 0) {
+ const {
+ name,
+ error: {
+ output: {
+ payload: {
+ message,
+ },
+ },
+ },
+ } = errors[0];
+
+ const title = getErrorTitle(errors.length, name);
+ toastNotifications.addDanger({
+ title,
+ text: message,
+ });
}
- if(removalSuccesses.length > 0) {
- if (removalSuccesses.length === 1) {
+ if (itemsDeleted.length > 0) {
+ if (itemsDeleted.length === 1) {
toastNotifications.addSuccess(i18n.translate('xpack.remoteClusters.removeAction.successSingleNotificationTitle', {
defaultMessage: `Remote cluster '{name}' was removed`,
- values: { name: removalSuccesses[0] },
+ values: { name: itemsDeleted[0] },
}));
} else {
toastNotifications.addSuccess(i18n.translate('xpack.remoteClusters.removeAction.successMultipleNotificationTitle', {
defaultMessage: '{count} remote clusters were removed',
- values: { count: names.length },
+ values: { count: itemsDeleted.length },
}));
}
}
@@ -76,6 +102,6 @@ export const removeClusters = (names) => async (dispatch, getState) => {
type: REMOVE_CLUSTERS_FINISH,
// Send the cluster that have been removed to the reducers
// and update the store immediately without the need to re-fetch from the server
- payload: removalSuccesses,
+ payload: itemsDeleted,
});
};
diff --git a/x-pack/plugins/remote_clusters/server/routes/api/remote_clusters/register_delete_route.js b/x-pack/plugins/remote_clusters/server/routes/api/remote_clusters/register_delete_route.js
index 586925d26a74d4..0071324cd6ed31 100644
--- a/x-pack/plugins/remote_clusters/server/routes/api/remote_clusters/register_delete_route.js
+++ b/x-pack/plugins/remote_clusters/server/routes/api/remote_clusters/register_delete_route.js
@@ -18,45 +18,83 @@ export function registerDeleteRoute(server) {
const licensePreRouting = licensePreRoutingFactory(server);
server.route({
- path: '/api/remote_clusters/{name}',
+ path: '/api/remote_clusters/{nameOrNames}',
method: 'DELETE',
+ config: {
+ pre: [ licensePreRouting ]
+ },
handler: async (request) => {
const callWithRequest = callWithRequestFactory(server, request);
- const { name } = request.params;
+ const { nameOrNames } = request.params;
+ const names = nameOrNames.split(',');
+
+ const itemsDeleted = [];
+ const errors = [];
- // Check if cluster does exist
- try {
- const existingCluster = await doesClusterExist(callWithRequest, name);
- if(!existingCluster) {
- return wrapCustomError(new Error('There is no remote cluster with that name.'), 404);
+ // Validator that returns an error if the remote cluster does not exist.
+ const validateClusterDoesExist = async (name) => {
+ try {
+ const existingCluster = await doesClusterExist(callWithRequest, name);
+ if (!existingCluster) {
+ return wrapCustomError(new Error('There is no remote cluster with that name.'), 404);
+ }
+ } catch (error) {
+ return wrapCustomError(error, 400);
}
- } catch (err) {
- return wrapCustomError(err, 400);
- }
-
- try {
- const deleteClusterPayload = serializeCluster({ name });
- const response = await callWithRequest('cluster.putSettings', { body: deleteClusterPayload });
- const acknowledged = get(response, 'acknowledged');
- const cluster = get(response, `persistent.cluster.remote.${name}`);
-
- if (acknowledged && !cluster) {
- return { success: true };
+ };
+
+ // Send the request to delete the cluster and return an error if it could not be deleted.
+ const sendRequestToDeleteCluster = async (name) => {
+ try {
+ const body = serializeCluster({ name });
+ const response = await callWithRequest('cluster.putSettings', { body });
+ const acknowledged = get(response, 'acknowledged');
+ const cluster = get(response, `persistent.cluster.remote.${name}`);
+
+ if (acknowledged && !cluster) {
+ return null;
+ }
+
+ // If for some reason the ES response still returns the cluster information,
+ // return an error. This shouldn't happen.
+ return wrapCustomError(new Error('Unable to delete cluster, information still returned from ES.'), 400);
+ } catch (error) {
+ if (isEsError(error)) {
+ return wrapEsError(error);
+ }
+
+ return wrapUnknownError(error);
}
+ };
- // If for some reason the ES response still returns the cluster information,
- // return an error. This shouldn't happen.
- return wrapCustomError(new Error('Unable to delete cluster, information still returned from ES.'), 400);
- } catch (err) {
- if (isEsError(err)) {
- return wrapEsError(err);
+ const deleteCluster = async (clusterName) => {
+ try {
+ // Validate that the cluster exists
+ let error = await validateClusterDoesExist(clusterName);
+
+ if (!error) {
+ // Delete the cluster
+ error = await sendRequestToDeleteCluster(clusterName);
+ }
+
+ if (error) {
+ throw error;
+ }
+
+ // If we are here, it means that everything went well...
+ itemsDeleted.push(clusterName);
+ } catch (error) {
+ errors.push({ name: clusterName, error });
}
+ };
- return wrapUnknownError(err);
- }
- },
- config: {
- pre: [ licensePreRouting ]
+ // Delete all our cluster in parallel
+ await Promise.all(names.map(deleteCluster));
+
+ return {
+ itemsDeleted,
+ errors,
+ };
}
});
}
diff --git a/x-pack/plugins/remote_clusters/server/routes/api/remote_clusters/register_delete_route.test.js b/x-pack/plugins/remote_clusters/server/routes/api/remote_clusters/register_delete_route.test.js
index 8c90b5bb0cf54e..f60a99eebf779b 100644
--- a/x-pack/plugins/remote_clusters/server/routes/api/remote_clusters/register_delete_route.test.js
+++ b/x-pack/plugins/remote_clusters/server/routes/api/remote_clusters/register_delete_route.test.js
@@ -48,11 +48,11 @@ describe('[API Routes] Remote Clusters Delete', () => {
registerDeleteRoute(server);
const response = await routeHandler({
params: {
- name: 'test_cluster'
+ nameOrNames: 'test_cluster'
}
});
- expect(response).toEqual({ success: true });
+ expect(response).toEqual({ errors: [], itemsDeleted: ['test_cluster'] });
});
it('should return an error if the response does still contain cluster information', async () => {
@@ -74,11 +74,14 @@ describe('[API Routes] Remote Clusters Delete', () => {
registerDeleteRoute(server);
const response = await routeHandler({
params: {
- name: 'test_cluster'
+ nameOrNames: 'test_cluster'
}
});
- expect(response).toEqual(wrapCustomError(new Error('Unable to delete cluster, information still returned from ES.'), 400));
+ expect(response.errors).toEqual([{
+ name: 'test_cluster',
+ error: wrapCustomError(new Error('Unable to delete cluster, information still returned from ES.'), 400),
+ }]);
});
it('should return an error if the cluster does not exist', async () => {
@@ -86,11 +89,14 @@ describe('[API Routes] Remote Clusters Delete', () => {
registerDeleteRoute(server);
const response = await routeHandler({
params: {
- name: 'test_cluster'
+ nameOrNames: 'test_cluster'
}
});
- expect(response).toEqual(wrapCustomError(new Error('There is no remote cluster with that name.'), 404));
+ expect(response.errors).toEqual([{
+ name: 'test_cluster',
+ error: wrapCustomError(new Error('There is no remote cluster with that name.'), 404),
+ }]);
});
it('should forward an ES error', async () => {
@@ -101,10 +107,13 @@ describe('[API Routes] Remote Clusters Delete', () => {
registerDeleteRoute(server);
const response = await routeHandler({
params: {
- name: 'test_cluster'
+ nameOrNames: 'test_cluster'
}
});
- expect(response).toEqual(Boom.boomify(mockError));
+ expect(response.errors).toEqual([{
+ name: 'test_cluster',
+ error: Boom.boomify(mockError),
+ }]);
});
});
diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.js b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.js
index 1182db65b4b9a0..b7a3d085a84979 100644
--- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.js
+++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.js
@@ -20,10 +20,11 @@ const compactWhitespace = str => {
};
export class HeadlessChromiumDriverFactory {
- constructor(binaryPath, logger, browserConfig) {
+ constructor(binaryPath, logger, browserConfig, queueTimeout) {
this.binaryPath = binaryPath;
this.logger = logger.clone(['chromium-driver-factory']);
this.browserConfig = browserConfig;
+ this.queueTimeout = queueTimeout;
}
type = 'chromium';
@@ -82,6 +83,12 @@ export class HeadlessChromiumDriverFactory {
});
page = await browser.newPage();
+
+ // All navigation/waitFor methods default to 30 seconds,
+ // which can cause the job to fail even if we bump timeouts in
+ // the config. Help alleviate errors like
+ // "TimeoutError: waiting for selector ".application" failed: timeout 30000ms exceeded"
+ page.setDefaultTimeout(this.queueTimeout);
} catch (err) {
observer.error(new Error(`Error spawning Chromium browser: [${err}]`));
throw err;
diff --git a/x-pack/plugins/reporting/server/browsers/chromium/index.js b/x-pack/plugins/reporting/server/browsers/chromium/index.js
index 24c452a1629411..3ff480d98dc6c3 100644
--- a/x-pack/plugins/reporting/server/browsers/chromium/index.js
+++ b/x-pack/plugins/reporting/server/browsers/chromium/index.js
@@ -8,10 +8,10 @@ import { HeadlessChromiumDriverFactory } from './driver_factory';
export { paths } from './paths';
-export async function createDriverFactory(binaryPath, logger, browserConfig) {
+export async function createDriverFactory(binaryPath, logger, browserConfig, queueTimeout) {
if (browserConfig.disableSandbox) {
logger.warning(`Enabling the Chromium sandbox provides an additional layer of protection.`);
}
- return new HeadlessChromiumDriverFactory(binaryPath, logger, browserConfig);
+ return new HeadlessChromiumDriverFactory(binaryPath, logger, browserConfig, queueTimeout);
}
diff --git a/x-pack/plugins/reporting/server/browsers/create_browser_driver_factory.js b/x-pack/plugins/reporting/server/browsers/create_browser_driver_factory.js
index 0a89fc9d7e77eb..6c17b4867a970c 100644
--- a/x-pack/plugins/reporting/server/browsers/create_browser_driver_factory.js
+++ b/x-pack/plugins/reporting/server/browsers/create_browser_driver_factory.js
@@ -17,13 +17,14 @@ export async function createBrowserDriverFactory(server) {
const BROWSER_TYPE = CAPTURE_CONFIG.browser.type;
const BROWSER_AUTO_DOWNLOAD = CAPTURE_CONFIG.browser.autoDownload;
const BROWSER_CONFIG = CAPTURE_CONFIG.browser[BROWSER_TYPE];
+ const REPORTING_TIMEOUT = config.get('xpack.reporting.queue.timeout');
if (BROWSER_AUTO_DOWNLOAD) {
await ensureBrowserDownloaded(BROWSER_TYPE);
}
try {
- const browserDriverFactory = await installBrowser(logger, BROWSER_CONFIG, BROWSER_TYPE, DATA_DIR);
+ const browserDriverFactory = await installBrowser(logger, BROWSER_CONFIG, BROWSER_TYPE, DATA_DIR, REPORTING_TIMEOUT);
logger.debug(`Browser installed at ${browserDriverFactory.binaryPath}`);
return browserDriverFactory;
} catch (error) {
diff --git a/x-pack/plugins/reporting/server/browsers/install.js b/x-pack/plugins/reporting/server/browsers/install.js
index ebd29c061d8d34..db51de1f24ed6d 100644
--- a/x-pack/plugins/reporting/server/browsers/install.js
+++ b/x-pack/plugins/reporting/server/browsers/install.js
@@ -22,7 +22,7 @@ const chmod = promisify(fs.chmod);
* @param {String} installsPath
* @return {Promise}
*/
-export async function installBrowser(logger, browserConfig, browserType, installsPath) {
+export async function installBrowser(logger, browserConfig, browserType, installsPath, queueTimeout) {
const browser = BROWSERS_BY_TYPE[browserType];
const pkg = browser.paths.packages.find(p => p.platforms.includes(process.platform));
@@ -40,5 +40,5 @@ export async function installBrowser(logger, browserConfig, browserType, install
await chmod(binaryPath, '755');
}
- return browser.createDriverFactory(binaryPath, logger, browserConfig);
+ return browser.createDriverFactory(binaryPath, logger, browserConfig, queueTimeout);
}
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index c0a8ac9932c750..379d31dec6702b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -1558,16 +1558,15 @@
"kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "全部导出",
"kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription": "选择要导出的类型。括号中的数字表示可导出多少此类型的对象。",
"kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle": "导出 {filteredItemCount, plural, one{# 个对象} other {# 个对象}}",
- "kbn.management.objects.objectsTable.flyout.confirmImport.resolvingConflictsLoadingMessage": "正在解决冲突……",
- "kbn.management.objects.objectsTable.flyout.confirmImport.retryingFailedObjectsLoadingMessage": "正在重试失败的对象……",
- "kbn.management.objects.objectsTable.flyout.confirmImport.savedSearchAreLinkedProperlyLoadingMessage": "确保已保存搜索已正确链接……",
- "kbn.management.objects.objectsTable.flyout.confirmImport.savingConflictsLoadingMessage": "正在保存冲突……",
+ "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage": "正在解决冲突……",
+ "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "正在重试失败的对象……",
+ "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "确保已保存搜索已正确链接……",
+ "kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "正在保存冲突……",
"kbn.management.objects.objectsTable.flyout.errorCalloutTitle": "抱歉,出现了错误",
"kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel": "取消",
"kbn.management.objects.objectsTable.flyout.import.confirmButtonLabel": "导入",
"kbn.management.objects.objectsTable.flyout.importFailedDescription": "{totalImportCount} 个对象中有 {failedImportCount} 个无法导入。导入失败",
"kbn.management.objects.objectsTable.flyout.importFailedTitle": "导入失败",
- "kbn.management.objects.objectsTable.flyout.importFileErrorMessage": "无法处理该文件。",
"kbn.management.objects.objectsTable.flyout.importPromptText": "导入",
"kbn.management.objects.objectsTable.flyout.importSavedObjectTitle": "导入已保存对象",
"kbn.management.objects.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel": "确认所有更改",
@@ -4268,49 +4267,12 @@
"xpack.infra.metricDetailPage.podMetricsLayout.overviewSection.sectionLabel": "概览",
"xpack.infra.metrics.layoutLabelOverviewTitle": "{layoutLabel} 概览",
"xpack.infra.metrics.loadingNodeDataText": "正在加载数据",
- "xpack.infra.metricsTimeControls.autoRefreshButtonLabel": "自动刷新",
- "xpack.infra.metricsTimeControls.cancelButtonLabel": "取消",
- "xpack.infra.metricsTimeControls.goButtonLabel": "执行",
- "xpack.infra.metricsTimeControls.resetButtonLabel": "重置",
- "xpack.infra.metricsTimeControls.stopRefreshingButtonLabel": "停止刷新",
"xpack.infra.nodeContextMenu.viewAPMTraces": "查看 {nodeType} APM 跟踪",
"xpack.infra.nodeContextMenu.viewLogsName": "查看日志",
"xpack.infra.nodeContextMenu.viewMetricsName": "查看指标",
"xpack.infra.nodesToWaffleMap.groupsWithGroups.allName": "全部",
"xpack.infra.nodesToWaffleMap.groupsWithNodes.allName": "全部",
"xpack.infra.notFoundPage.noContentFoundErrorTitle": "未找到任何内容",
- "xpack.infra.rangeDatePicker.applyFormRowButtonLabel": "应用",
- "xpack.infra.rangeDatePicker.countOfFormRowAriaLabel": "计数 -",
- "xpack.infra.rangeDatePicker.dateQuickSelectAriaLabel": "日期快速选择",
- "xpack.infra.rangeDatePicker.endDateAriaLabel": "结束日期",
- "xpack.infra.rangeDatePicker.lastQuickSelectTimeText": "过去 {quickSelectTime} {quickSelectUnit}",
- "xpack.infra.rangeDatePicker.lastQuickSelectTitle": "最后",
- "xpack.infra.rangeDatePicker.monthToDateText": "本月迄今为止",
- "xpack.infra.rangeDatePicker.pluralUnitOptions.daysLabel": "天",
- "xpack.infra.rangeDatePicker.pluralUnitOptions.hoursLabel": "小时",
- "xpack.infra.rangeDatePicker.pluralUnitOptions.minutesLabel": "分钟",
- "xpack.infra.rangeDatePicker.pluralUnitOptions.monthsLabel": "个月",
- "xpack.infra.rangeDatePicker.pluralUnitOptions.secondsLabel": "秒",
- "xpack.infra.rangeDatePicker.pluralUnitOptions.weeksLabel": "周",
- "xpack.infra.rangeDatePicker.pluralUnitOptions.yearsLabel": "年",
- "xpack.infra.rangeDatePicker.quickSelectTitle": "快速选择",
- "xpack.infra.rangeDatePicker.recentlyUsedDateRangesTitle": "最近使用的日期范围",
- "xpack.infra.rangeDatePicker.renderCommonlyUsedLinksTitle": "常用",
- "xpack.infra.rangeDatePicker.singleUnitOptions.dayLabel": "天",
- "xpack.infra.rangeDatePicker.singleUnitOptions.hourLabel": "小时",
- "xpack.infra.rangeDatePicker.singleUnitOptions.minuteLabel": "分钟",
- "xpack.infra.rangeDatePicker.singleUnitOptions.monthLabel": "个月",
- "xpack.infra.rangeDatePicker.singleUnitOptions.secondLabel": "秒",
- "xpack.infra.rangeDatePicker.singleUnitOptions.weekLabel": "周",
- "xpack.infra.rangeDatePicker.singleUnitOptions.yearLabel": "年",
- "xpack.infra.rangeDatePicker.startDateAriaLabel": "开始日期",
- "xpack.infra.rangeDatePicker.thisMonthText": "本月",
- "xpack.infra.rangeDatePicker.thisWeekText": "本周",
- "xpack.infra.rangeDatePicker.thisYearText": "本年",
- "xpack.infra.rangeDatePicker.todayText": "今日",
- "xpack.infra.rangeDatePicker.weekToDateText": "本周迄今为止",
- "xpack.infra.rangeDatePicker.yearToDateText": "本年迄今为止",
- "xpack.infra.rangeDatePicker.yesterdayText": "昨天",
"xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage": "正在加载 {nodeType} 日志",
"xpack.infra.registerFeatures.infraOpsDescription": "浏览常用服务器、容器和服务的基础设施指标和日志。",
"xpack.infra.registerFeatures.infraOpsTitle": "基础设施",
@@ -8134,4 +8096,4 @@
"xpack.watcher.watchActionsTitle": "满足后将执行 {watchActionsCount, plural, one{# 个操作} other {# 个操作}}",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/upgrade_assistant/public/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/components/tabs.tsx
index b99d05ec935690..76cc1e33ca06b2 100644
--- a/x-pack/plugins/upgrade_assistant/public/components/tabs.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/components/tabs.tsx
@@ -26,13 +26,19 @@ import { CheckupTab } from './tabs/checkup';
import { OverviewTab } from './tabs/overview';
import { LoadingState, TelemetryState, UpgradeAssistantTabProps } from './types';
+enum ClusterUpgradeState {
+ needsUpgrade,
+ partiallyUpgraded,
+ upgraded,
+}
+
interface TabsState {
loadingState: LoadingState;
loadingError?: Error;
checkupData?: UpgradeAssistantStatus;
selectedTabIndex: number;
telemetryState: TelemetryState;
- upgradeableCluster: boolean;
+ clusterUpgradeState: ClusterUpgradeState;
}
export class UpgradeAssistantTabsUI extends React.Component<
@@ -44,7 +50,7 @@ export class UpgradeAssistantTabsUI extends React.Component<
this.state = {
loadingState: LoadingState.Loading,
- upgradeableCluster: true,
+ clusterUpgradeState: ClusterUpgradeState.needsUpgrade,
selectedTabIndex: 0,
telemetryState: TelemetryState.Complete,
};
@@ -58,10 +64,10 @@ export class UpgradeAssistantTabsUI extends React.Component<
}
public render() {
- const { selectedTabIndex, telemetryState, upgradeableCluster } = this.state;
+ const { selectedTabIndex, telemetryState, clusterUpgradeState } = this.state;
const tabs = this.tabs;
- if (!upgradeableCluster) {
+ if (clusterUpgradeState === ClusterUpgradeState.partiallyUpgraded) {
return (
@@ -80,7 +86,33 @@ export class UpgradeAssistantTabsUI extends React.Component<
+
+ }
+ />
+
+
+ );
+ } else if (clusterUpgradeState === ClusterUpgradeState.upgraded) {
+ return (
+
+
+
+
+
+ }
+ body={
+
+
}
@@ -134,7 +166,9 @@ export class UpgradeAssistantTabsUI extends React.Component<
if (get(e, 'response.status') === 426) {
this.setState({
loadingState: LoadingState.Success,
- upgradeableCluster: false,
+ clusterUpgradeState: get(e, 'response.data.attributes.allNodesUpgraded', false)
+ ? ClusterUpgradeState.upgraded
+ : ClusterUpgradeState.partiallyUpgraded,
});
} else {
this.setState({ loadingState: LoadingState.Error, loadingError: e });
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts
index e4735a5e7adf8e..9a0fca6d4139c2 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts
@@ -71,11 +71,11 @@ describe('EsVersionPrecheck', () => {
);
});
- it('throws a 426 message when nodes are not on same version', async () => {
+ it('throws a 426 message w/ allNodesUpgraded = false when nodes are not on same version', async () => {
const fakeCallWithRequest = jest.fn().mockResolvedValue({
nodes: {
node1: { version: CURRENT_VERSION.raw },
- node2: { version: CURRENT_VERSION.inc('major').raw },
+ node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw },
},
});
const fakeGetCluster = jest.fn(() => ({ callWithRequest: fakeCallWithRequest }));
@@ -83,12 +83,31 @@ describe('EsVersionPrecheck', () => {
server: { plugins: { elasticsearch: { getCluster: fakeGetCluster } } },
} as any;
- await expect(EsVersionPrecheck.method(fakeRequest, {} as any)).rejects.toHaveProperty(
- 'output.statusCode',
- 426
+ const result = EsVersionPrecheck.method(fakeRequest, {} as any);
+ await expect(result).rejects.toHaveProperty('output.statusCode', 426);
+ await expect(result).rejects.toHaveProperty(
+ 'output.payload.attributes.allNodesUpgraded',
+ false
);
});
+ it('throws a 426 message w/ allNodesUpgraded = true when nodes are on next version', async () => {
+ const fakeCallWithRequest = jest.fn().mockResolvedValue({
+ nodes: {
+ node1: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw },
+ node2: { version: new SemVer(CURRENT_VERSION.raw).inc('major').raw },
+ },
+ });
+ const fakeGetCluster = jest.fn(() => ({ callWithRequest: fakeCallWithRequest }));
+ const fakeRequest = {
+ server: { plugins: { elasticsearch: { getCluster: fakeGetCluster } } },
+ } as any;
+
+ const result = EsVersionPrecheck.method(fakeRequest, {} as any);
+ await expect(result).rejects.toHaveProperty('output.statusCode', 426);
+ await expect(result).rejects.toHaveProperty('output.payload.attributes.allNodesUpgraded', true);
+ });
+
it('returns true when nodes are on same version', async () => {
const fakeCallWithRequest = jest.fn().mockResolvedValue({
nodes: {
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts
index 6cb913d6dd582c..d84d5f54444723 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts
@@ -31,15 +31,21 @@ export const getAllNodeVersions = async (callCluster: CallCluster) => {
export const verifyAllMatchKibanaVersion = (allNodeVersions: SemVer[]) => {
// Determine if all nodes in the cluster are running the same major version as Kibana.
- const anyDifferentEsNodes = !!allNodeVersions.find(
+ const numDifferentVersion = allNodeVersions.filter(
esNodeVersion => esNodeVersion.major !== CURRENT_VERSION.major
- );
+ ).length;
+ const numSameVersion = allNodeVersions.filter(
+ esNodeVersion => esNodeVersion.major === CURRENT_VERSION.major
+ ).length;
- if (anyDifferentEsNodes) {
- throw new Boom(`There are some nodes running a different version of Elasticsearch`, {
+ if (numDifferentVersion) {
+ const error = new Boom(`There are some nodes running a different version of Elasticsearch`, {
// 426 means "Upgrade Required" and is used when semver compatibility is not met.
statusCode: 426,
});
+
+ error.output.payload.attributes = { allNodesUpgraded: !numSameVersion };
+ throw error;
}
};
diff --git a/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js b/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js
index 6651a642fdc1d6..5f8c769cb2b5bb 100644
--- a/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js
+++ b/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js
@@ -48,6 +48,7 @@ export default function ({ getService }) {
isConfiguredByNode: false,
});
});
+
it('should not allow us to re-add an existing remote cluster', async () => {
const uri = `${API_BASE_PATH}`;
@@ -142,7 +143,84 @@ export default function ({ getService }) {
.set('kbn-xsrf', 'xxx')
.expect(200);
- expect(body).to.eql({ success: true });
+ expect(body).to.eql({
+ itemsDeleted: ['test_cluster'],
+ errors: [],
+ });
+ });
+
+ it('should allow us to delete multiple remote clusters', async () => {
+ // Create clusters to delete.
+ await supertest
+ .post(API_BASE_PATH)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ name: 'test_cluster1',
+ seeds: [
+ NODE_SEED
+ ],
+ skipUnavailable: true,
+ })
+ .expect(200);
+
+ await supertest
+ .post(API_BASE_PATH)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ name: 'test_cluster2',
+ seeds: [
+ NODE_SEED
+ ],
+ skipUnavailable: true,
+ })
+ .expect(200);
+
+ const uri = `${API_BASE_PATH}/test_cluster1,test_cluster2`;
+
+ const {
+ body: { itemsDeleted, errors }
+ } = await supertest
+ .delete(uri)
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ expect(errors).to.eql([]);
+
+ // The order isn't guaranteed, so we assert against individual names instead of asserting
+ // against the value of the array itself.
+ ['test_cluster1', 'test_cluster2'].forEach(clusterName => {
+ expect(itemsDeleted.includes(clusterName)).to.be(true);
+ });
+ });
+
+ it(`should tell us which remote clusters couldn't be deleted`, async () => {
+ const uri = `${API_BASE_PATH}/test_cluster_doesnt_exist`;
+
+ const { body } = await supertest
+ .delete(uri)
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ expect(body).to.eql({
+ itemsDeleted: [],
+ errors: [{
+ name: 'test_cluster_doesnt_exist',
+ error: {
+ isBoom: true,
+ isServer: false,
+ data: null,
+ output: {
+ statusCode: 404,
+ payload: {
+ statusCode: 404,
+ error: 'Not Found',
+ message: 'There is no remote cluster with that name.',
+ },
+ headers: {},
+ },
+ },
+ }],
+ });
});
});
});
diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts
index 5ccb42ac2982eb..2e71d4a50f85e1 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/import.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts
@@ -112,7 +112,11 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
await supertest
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`)
.auth(user.username, user.password)
- .attach('file', Buffer.from(JSON.stringify(data), 'utf8'), 'export.ndjson')
+ .attach(
+ 'file',
+ Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'),
+ 'export.ndjson'
+ )
.expect(tests.default.statusCode)
.then(tests.default.response);
});
@@ -131,7 +135,11 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`)
.query({ overwrite: true })
.auth(user.username, user.password)
- .attach('file', Buffer.from(JSON.stringify(data), 'utf8'), 'export.ndjson')
+ .attach(
+ 'file',
+ Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'),
+ 'export.ndjson'
+ )
.expect(tests.unknownType.statusCode)
.then(tests.unknownType.response);
});
diff --git a/x-pack/test_utils/testbed/testbed.js b/x-pack/test_utils/testbed/testbed.js
index 30bd82c710d875..6d64ad35a89d7a 100644
--- a/x-pack/test_utils/testbed/testbed.js
+++ b/x-pack/test_utils/testbed/testbed.js
@@ -94,7 +94,11 @@ export const registerTestBed = (Component, defaultProps, store = {}) => (props,
};
const setInputValue = (inputTestSubject, value, isAsync = false) => {
- const formInput = find(inputTestSubject);
+ const formInput = typeof inputTestSubject === 'string'
+ ? find(inputTestSubject)
+ : inputTestSubject;
+
+
formInput.simulate('change', { target: { value } });
component.update();
diff --git a/yarn.lock b/yarn.lock
index 5717bc65594dce..3f503637eb4d2f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1339,10 +1339,10 @@
tabbable "^1.1.0"
uuid "^3.1.0"
-"@elastic/eui@9.8.0":
- version "9.8.0"
- resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-9.8.0.tgz#669061a2bbd2ccb535f4550295128357aea6ec46"
- integrity sha512-QsEnmA5Xokj8IJZQGt1+8huDuaGpOpPyJNLCvluzRt1Zvc09ooxc0yisUqny8cUU0CxiJGfn790G1VR+m3BAQQ==
+"@elastic/eui@9.9.0":
+ version "9.9.0"
+ resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-9.9.0.tgz#bbe1a3d3cb22f1198e05665ca52cd48335a73b53"
+ integrity sha512-T6Cr68Il8tfbke/361UUW6gyQXEQwAvmDIUYOZSqCjsYjSq6JTFbwzQ6ezRD11RMN2vn8CPUwPVP5JAGNUXz8Q==
dependencies:
"@types/lodash" "^4.14.116"
"@types/numeral" "^0.0.25"