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 ( + + +
+ + + + + + + + {this.renderDuplicateTitleCallout()} + + + {this.renderCopyOnSave()} + + + } + > + + + + {this.props.options} + + + + + + + - componentWillUnmount() { - this._isMounted = false; + + {this.props.confirmButtonLabel ? ( + this.props.confirmButtonLabel + ) : ( + + )} + + +
+
+
+ ); } - 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 ( - - -
- - - - - - - - - {this.renderDuplicateTitleCallout()} - - - - {this.renderCopyOnSave()} - - )} - > - - - - {this.props.options} - - - - - - - - - - - - {this.props.confirmButtonLabel - ? this.props.confirmButtonLabel - : ( - - ) - } - - -
-
-
- ); - } } - -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 [ />