Skip to content

Commit

Permalink
Legacy SO import: Fix bug causing multiple overrides to only show the…
Browse files Browse the repository at this point in the history
… last confirm modal (#76482) (#76598)

* Legacy SO import: Fix bug causing multiple overrides to only show the last confirm modal

* eslint

* fix for loops
  • Loading branch information
pgayvallet authored Sep 3, 2020
1 parent de3a24a commit f7dff1f
Show file tree
Hide file tree
Showing 7 changed files with 379 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,6 @@ function groupByType(docs: SavedObjectsRawDoc[]): Record<string, SavedObjectsRaw
}, defaultDocTypes);
}

async function awaitEachItemInParallel<T, R>(list: T[], op: (item: T) => R) {
return await Promise.all(list.map((item) => op(item)));
}

export async function resolveIndexPatternConflicts(
resolutions: Array<{ oldId: string; newId: string }>,
conflictedIndexPatterns: any[],
Expand All @@ -175,7 +171,7 @@ export async function resolveIndexPatternConflicts(
) {
let importCount = 0;

await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj, doc }) => {
for (const { obj, doc } of conflictedIndexPatterns) {
const serializedSearchSource = JSON.parse(
doc._source.kibanaSavedObjectMeta?.searchSourceJSON || '{}'
);
Expand Down Expand Up @@ -220,25 +216,25 @@ export async function resolveIndexPatternConflicts(

if (!allResolved) {
// The user decided to skip this conflict so do nothing
return;
continue;
}
obj.searchSource = await dependencies.search.searchSource.create(
serializedSearchSourceWithInjectedReferences
);
if (await saveObject(obj, overwriteAll)) {
importCount++;
}
});
}
return importCount;
}

export async function saveObjects(objs: SavedObject[], overwriteAll: boolean) {
let importCount = 0;
await awaitEachItemInParallel(objs, async (obj) => {
for (const obj of objs) {
if (await saveObject(obj, overwriteAll)) {
importCount++;
}
});
}
return importCount;
}

Expand All @@ -253,16 +249,16 @@ export async function resolveSavedSearches(
overwriteAll: boolean
) {
let importCount = 0;
await awaitEachItemInParallel(savedSearches, async (searchDoc) => {
for (const searchDoc of savedSearches) {
const obj = await getSavedObject(searchDoc, services);
if (!obj) {
// Just ignore?
return;
continue;
}
if (await importDocument(obj, searchDoc, overwriteAll)) {
importCount++;
}
});
}
return importCount;
}

Expand All @@ -280,7 +276,10 @@ export async function resolveSavedObjects(
let importedObjectCount = 0;
const failedImports: FailedImport[] = [];
// Start with the index patterns since everything is dependent on them
await awaitEachItemInParallel(docTypes.indexPatterns, async (indexPatternDoc) => {
// As the confirmation opens a modal, and as we only allow one modal at a time
// (opening a new one close the previous with a rejection)
// we can't do that in parallel
for (const indexPatternDoc of docTypes.indexPatterns) {
try {
const importedIndexPatternId = await importIndexPattern(
indexPatternDoc,
Expand All @@ -294,7 +293,7 @@ export async function resolveSavedObjects(
} catch (error) {
failedImports.push({ obj: indexPatternDoc as any, error });
}
});
}

// 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 dependent on those saved searches existing
Expand All @@ -311,7 +310,7 @@ export async function resolveSavedObjects(
// likely that these saved objects will work once resaved so keep them around to resave them.
const conflictedSavedObjectsLinkedToSavedSearches: any[] = [];

await awaitEachItemInParallel(docTypes.searches, async (searchDoc) => {
for (const searchDoc of docTypes.searches) {
const obj = await getSavedObject(searchDoc, services);

try {
Expand All @@ -329,9 +328,9 @@ export async function resolveSavedObjects(
failedImports.push({ obj, error });
}
}
});
}

await awaitEachItemInParallel(docTypes.other, async (otherDoc) => {
for (const otherDoc of docTypes.other) {
const obj = await getSavedObject(otherDoc, services);

try {
Expand All @@ -350,7 +349,7 @@ export async function resolveSavedObjects(
failedImports.push({ obj, error });
}
}
});
}

return {
conflictedIndexPatterns,
Expand Down
54 changes: 52 additions & 2 deletions test/functional/apps/management/_import_objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import expect from '@kbn/expect';
import path from 'path';
import { keyBy } from 'lodash';

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
Expand Down Expand Up @@ -203,12 +205,12 @@ export default function ({ getService, getPageObjects }) {
// delete .kibana index and then wait for Kibana to re-create it
await kibanaServer.uiSettings.replace({});
await PageObjects.settings.navigateTo();
await esArchiver.load('management');
await esArchiver.load('saved_objects_imports');
await PageObjects.settings.clickKibanaSavedObjects();
});

afterEach(async function () {
await esArchiver.unload('management');
await esArchiver.unload('saved_objects_imports');
});

it('should import saved objects', async function () {
Expand Down Expand Up @@ -280,6 +282,54 @@ export default function ({ getService, getPageObjects }) {
expect(isSuccessful).to.be(true);
});

it('should allow the user to confirm overriding multiple duplicate saved objects', async function () {
// 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.savedObjects.importFile(
path.join(__dirname, 'exports', '_import_objects_multiple_exists.json'),
false
);

await PageObjects.savedObjects.checkImportLegacyWarning();
await PageObjects.savedObjects.checkImportConflictsWarning();

await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
await PageObjects.savedObjects.clickConfirmChanges();

// Override the visualizations.
await PageObjects.common.clickConfirmOnModal(false);
// as the second confirm can pop instantly, we can't wait for it to be hidden
// with is why we call clickConfirmOnModal with ensureHidden: false in previous statement
// but as the initial popin can take a few ms before fading, we need to wait a little
// to avoid clicking twice on the same modal.
await delay(1000);
await PageObjects.common.clickConfirmOnModal(false);

const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
expect(isSuccessful).to.be(true);
});

it('should allow the user to confirm overriding multiple duplicate index patterns', async function () {
// 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.savedObjects.importFile(
path.join(__dirname, 'exports', '_import_index_patterns_multiple_exists.json'),
false
);

// Override the index patterns.
await PageObjects.common.clickConfirmOnModal(false);
// as the second confirm can pop instantly, we can't wait for it to be hidden
// with is why we call clickConfirmOnModal with ensureHidden: false in previous statement
// but as the initial popin can take a few ms before fading, we need to wait a little
// to avoid clicking twice on the same modal.
await delay(1000);
await PageObjects.common.clickConfirmOnModal(false);

const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
expect(isSuccessful).to.be(true);
});

it('should import saved objects linked to saved searches', async function () {
await PageObjects.savedObjects.importFile(
path.join(__dirname, 'exports', '_import_objects_saved_search.json')
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[
{
"_id": "test-1",
"_type": "visualization",
"_source": {
"title": "Visualization test 1",
"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
}
},
{
"_id": "test-2",
"_type": "visualization",
"_source": {
"title": "Visualization test 2",
"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
}
}
]
Binary file not shown.
Loading

0 comments on commit f7dff1f

Please sign in to comment.