From f93b76285b0fec16306a1c28b3fe91d8d80d8b14 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 2 Feb 2018 14:28:29 -0500 Subject: [PATCH] [Management] Fix importing objects connected to saved searches that contain conflicts (#16004) * Handle cases of importing objects that are connected to searches, but error out due to the searches index pattern not found * Add tests and fix flaky ones --- .../management/sections/indices/index.html | 6 +- .../management/sections/objects/_objects.js | 18 ++- .../apps/management/_import_objects.js | 130 +++++++++--------- ...rt_objects_connected_to_saved_search.json} | 23 ---- .../exports/_import_objects_exists.json | 19 +++ .../exports/_import_objects_saved_search.json | 25 ++++ test/functional/page_objects/settings_page.js | 4 + 7 files changed, 136 insertions(+), 89 deletions(-) rename test/functional/apps/management/exports/{_import_objects_with_saved_searches.json => _import_objects_connected_to_saved_search.json} (72%) create mode 100644 test/functional/apps/management/exports/_import_objects_exists.json create mode 100644 test/functional/apps/management/exports/_import_objects_saved_search.json diff --git a/src/core_plugins/kibana/public/management/sections/indices/index.html b/src/core_plugins/kibana/public/management/sections/indices/index.html index eb5a2449871e..f1e6cbed5cab 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/index.html +++ b/src/core_plugins/kibana/public/management/sections/indices/index.html @@ -31,7 +31,11 @@ ng-repeat="pattern in indexPatternList | orderBy:['-default','title'] track by pattern.id" class="sidebar-item" > - +
diff --git a/src/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/core_plugins/kibana/public/management/sections/objects/_objects.js index e7c809e47cec..4d5116b97e72 100644 --- a/src/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -213,6 +213,11 @@ uiModules.get('apps/management') // We want to do the same for saved searches, but we want to keep them separate because they need // to be applied _first_ because other saved objects can be depedent on those saved searches existing const conflictedSearchDocs = []; + // It's possbile to have saved objects that link to saved searches which then link to index patterns + // and those could error out, but the error comes as an index pattern not found error. We can't resolve + // those the same as way as normal index pattern not found errors, but when those are fixed, it's very + // likely that these saved objects will work once resaved so keep them around to resave them. + const conflictedSavedObjectsLinkedToSavedSearches = []; function importDocument(swallowErrors, doc) { const { service } = find($scope.services, { type: doc._type }) || {}; @@ -242,7 +247,11 @@ uiModules.get('apps/management') conflictedSearchDocs.push(doc); return; case 'index-pattern': - conflictedIndexPatterns.push({ obj, doc }); + if (obj.savedSearchId) { + conflictedSavedObjectsLinkedToSavedSearches.push(obj); + } else { + conflictedIndexPatterns.push({ obj, doc }); + } return; } } @@ -280,7 +289,11 @@ uiModules.get('apps/management') return; } return obj.hydrateIndexPattern(newIndexId) - .then(() => obj.save({ confirmOverwrite: !overwriteAll })); + .then(() => saveObject(obj)); + } + + function saveObject(obj) { + return obj.save({ confirmOverwrite: !overwriteAll }); } const docTypes = groupByType(docs); @@ -293,6 +306,7 @@ uiModules.get('apps/management') showChangeIndexModal( (objs) => { Promise.map(conflictedIndexPatterns, resolveConflicts.bind(null, objs)) + .then(Promise.map(conflictedSavedObjectsLinkedToSavedSearches, saveObject)) .then(resolve) .catch(reject); }, diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js index b8ef5d603e45..9cb5cc6d0f52 100644 --- a/test/functional/apps/management/_import_objects.js +++ b/test/functional/apps/management/_import_objects.js @@ -5,117 +5,121 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['common', 'settings']); + const PageObjects = getPageObjects(['common', 'settings', 'header']); describe('import objects', function describeIndexTests() { - before(async function () { + 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'); }); - after(async function () { + afterEach(async function () { await esArchiver.unload('management'); }); it('should import saved objects normally', async function () { - await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json')); await PageObjects.common.clickConfirmOnModal(); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.settings.clickVisualizationsTab(); const rowCount = await retry.try(async () => { const rows = await PageObjects.settings.getVisualizationRows(); - if (rows.length !== 2) { - throw 'Not loaded yet'; - } return rows.length; }); expect(rowCount).to.be(2); }); it('should import conflicts using a confirm modal', async function () { - await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects-conflicts.json')); await PageObjects.common.clickConfirmOnModal(); await PageObjects.settings.setImportIndexFieldOption(2); await PageObjects.settings.clickChangeIndexConfirmButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.settings.clickVisualizationsTab(); const rowCount = await retry.try(async () => { const rows = await PageObjects.settings.getVisualizationRows(); - if (rows.length !== 2) { - throw 'Not loaded yet'; - } return rows.length; }); expect(rowCount).to.be(2); }); - // Flaky: https://github.com/elastic/kibana/issues/15913 - // it('should allow for overrides', async function () { - // await PageObjects.settings.navigateTo(); - // await PageObjects.settings.clickKibanaSavedObjects(); - // await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json')); - // await PageObjects.common.clickConfirmOnModal(); - // await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json')); - // await PageObjects.common.clickConfirmOnModal(); - // await PageObjects.settings.clickVisualizationsTab(); - // const rowCount = await retry.try(async () => { - // const rows = await PageObjects.settings.getVisualizationRows(); - // if (rows.length !== 2) { - // throw 'Not loaded yet'; - // } - // return rows.length; - // }); - // expect(rowCount).to.be(2); - // }); - - // Flaky: https://github.com/elastic/kibana/issues/15913 - // it('should allow for cancelling overrides', async function () { - // await PageObjects.settings.navigateTo(); - // await PageObjects.settings.clickKibanaSavedObjects(); - // await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json')); - // await PageObjects.common.clickConfirmOnModal(); - // await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects.json')); - // await PageObjects.common.clickCancelOnModal(true); - // await PageObjects.common.clickConfirmOnModal(); - // await PageObjects.settings.clickVisualizationsTab(); - // const rowCount = await retry.try(async () => { - // const rows = await PageObjects.settings.getVisualizationRows(); - // if (rows.length !== 2) { - // throw 'Not loaded yet'; - // } - // return rows.length; - // }); - // expect(rowCount).to.be(2); - // }); - - it('should handle saved searches and objects with saved searches properly', async function () { - await PageObjects.settings.navigateTo(); + it('should allow for overrides', async function () { await PageObjects.settings.clickKibanaSavedObjects(); - await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_with_saved_searches.json')); - await PageObjects.common.clickConfirmOnModal(); + + // Put in data which already exists + await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json')); + // Say we want to be asked + await PageObjects.common.clickCancelOnModal(); + // Interact with the conflict modal await PageObjects.settings.setImportIndexFieldOption(2); await PageObjects.settings.clickChangeIndexConfirmButton(); - await PageObjects.settings.clickVisualizationsTab(); + // Now confirm we want to override + await PageObjects.common.clickConfirmOnModal(); + await PageObjects.header.waitUntilLoadingHasFinished(); - const vizRowCount = await retry.try(async () => { + await PageObjects.settings.clickVisualizationsTab(); + const rowCount = await retry.try(async () => { const rows = await PageObjects.settings.getVisualizationRows(); - if (rows.length !== 2) { - throw 'Not loaded yet'; - } return rows.length; }); - expect(vizRowCount).to.be(2); + expect(rowCount).to.be(1); + }); + + it('should allow for cancelling overrides', async function () { + await PageObjects.settings.clickKibanaSavedObjects(); + + // Put in data which already exists + await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json')); + // Say we want to be asked + await PageObjects.common.clickCancelOnModal(); + // Interact with the conflict modal + await PageObjects.settings.setImportIndexFieldOption(2); + await PageObjects.settings.clickChangeIndexConfirmButton(); + // Now cancel the override + await PageObjects.common.clickCancelOnModal(); + + await PageObjects.settings.clickVisualizationsTab(); + const rowCount = await retry.try(async () => { + const rows = await PageObjects.settings.getVisualizationRows(); + return rows.length; + }); + expect(rowCount).to.be(1); + }); + + it('should handle saved searches and objects with saved searches properly', 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.common.clickConfirmOnModal(); + + // Second, we need to delete the index pattern + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndices(); + await PageObjects.settings.clickOnOnlyIndexPattern(); + await PageObjects.settings.removeIndexPattern(); + + // Last, import a saved object connected to the saved search + // This should NOT show the modal + 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.common.clickConfirmOnModal(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.settings.clickVisualizationsTab(); + const vizRowCount = await retry.try(async () => { + const rows = await PageObjects.settings.getVisualizationRows(); + return rows.length; + }); + expect(vizRowCount).to.be(1); await PageObjects.settings.clickSearchesTab(); const searchRowCount = await retry.try(async () => { const rows = await PageObjects.settings.getVisualizationRows(); - if (rows.length !== 1) { - throw 'Not loaded yet'; - } return rows.length; }); expect(searchRowCount).to.be(1); diff --git a/test/functional/apps/management/exports/_import_objects_with_saved_searches.json b/test/functional/apps/management/exports/_import_objects_connected_to_saved_search.json similarity index 72% rename from test/functional/apps/management/exports/_import_objects_with_saved_searches.json rename to test/functional/apps/management/exports/_import_objects_connected_to_saved_search.json index a7348d1502eb..6711c1ffa888 100644 --- a/test/functional/apps/management/exports/_import_objects_with_saved_searches.json +++ b/test/functional/apps/management/exports/_import_objects_connected_to_saved_search.json @@ -1,27 +1,4 @@ [ - { - "_id": "c45e6c50-ba72-11e7-a8f9-ad70f02e633d", - "_type": "search", - "_source": { - "title": "PHP saved search", - "description": "", - "hits": 0, - "columns": [ - "_source" - ], - "sort": [ - "@timestamp", - "desc" - ], - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"f0df0960-ae8d-11e7-9c8d-53400275d89a\",\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"php\"},\"filter\":[]}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - }, { "_id": "cbd520f0-ba72-11e7-a8f9-ad70f02e633d", "_type": "visualization", diff --git a/test/functional/apps/management/exports/_import_objects_exists.json b/test/functional/apps/management/exports/_import_objects_exists.json new file mode 100644 index 000000000000..5356d1fdf647 --- /dev/null +++ b/test/functional/apps/management/exports/_import_objects_exists.json @@ -0,0 +1,19 @@ +[ + { + "_id": "Shared-Item-Visualization-AreaChart", + "_type": "visualization", + "_source": { + "title": "Shared-Item Visualization AreaChart", + "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\":{}}", + "uiStateJSON": "{}", + "description": "AreaChart", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + } +] diff --git a/test/functional/apps/management/exports/_import_objects_saved_search.json b/test/functional/apps/management/exports/_import_objects_saved_search.json new file mode 100644 index 000000000000..bfd034a7086d --- /dev/null +++ b/test/functional/apps/management/exports/_import_objects_saved_search.json @@ -0,0 +1,25 @@ +[ + { + "_id": "c45e6c50-ba72-11e7-a8f9-ad70f02e633d", + "_type": "search", + "_source": { + "title": "PHP saved search", + "description": "", + "hits": 0, + "columns": [ + "_source" + ], + "sort": [ + "@timestamp", + "desc" + ], + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"f1e4c910-a2e6-11e7-bb30-233be9be6a15\",\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"php\"},\"filter\":[]}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + } +] diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index 93f104fdae01..3430a9d0cfe8 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -346,6 +346,10 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return await testSubjects.find('createIndexPatternCreateButton'); } + async clickOnOnlyIndexPattern() { + return await testSubjects.click('indexPatternLink'); + } + async removeIndexPattern() { let alertText; await retry.try(async () => {