Skip to content

Commit 148459f

Browse files
authored
Legacy SO import: Fix bug causing multiple overrides to only show the last confirm modal (#76482) (#76597)
* Legacy SO import: Fix bug causing multiple overrides to only show the last confirm modal * eslint * fix for loops
1 parent d405da9 commit 148459f

File tree

7 files changed

+379
-22
lines changed

7 files changed

+379
-22
lines changed

src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,6 @@ function groupByType(docs: SavedObjectsRawDoc[]): Record<string, SavedObjectsRaw
160160
}, defaultDocTypes);
161161
}
162162

163-
async function awaitEachItemInParallel<T, R>(list: T[], op: (item: T) => R) {
164-
return await Promise.all(list.map((item) => op(item)));
165-
}
166-
167163
export async function resolveIndexPatternConflicts(
168164
resolutions: Array<{ oldId: string; newId: string }>,
169165
conflictedIndexPatterns: any[],
@@ -175,7 +171,7 @@ export async function resolveIndexPatternConflicts(
175171
) {
176172
let importCount = 0;
177173

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

221217
if (!allResolved) {
222218
// The user decided to skip this conflict so do nothing
223-
return;
219+
continue;
224220
}
225221
obj.searchSource = await dependencies.search.searchSource.create(
226222
serializedSearchSourceWithInjectedReferences
227223
);
228224
if (await saveObject(obj, overwriteAll)) {
229225
importCount++;
230226
}
231-
});
227+
}
232228
return importCount;
233229
}
234230

235231
export async function saveObjects(objs: SavedObject[], overwriteAll: boolean) {
236232
let importCount = 0;
237-
await awaitEachItemInParallel(objs, async (obj) => {
233+
for (const obj of objs) {
238234
if (await saveObject(obj, overwriteAll)) {
239235
importCount++;
240236
}
241-
});
237+
}
242238
return importCount;
243239
}
244240

@@ -253,16 +249,16 @@ export async function resolveSavedSearches(
253249
overwriteAll: boolean
254250
) {
255251
let importCount = 0;
256-
await awaitEachItemInParallel(savedSearches, async (searchDoc) => {
252+
for (const searchDoc of savedSearches) {
257253
const obj = await getSavedObject(searchDoc, services);
258254
if (!obj) {
259255
// Just ignore?
260-
return;
256+
continue;
261257
}
262258
if (await importDocument(obj, searchDoc, overwriteAll)) {
263259
importCount++;
264260
}
265-
});
261+
}
266262
return importCount;
267263
}
268264

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

299298
// We want to do the same for saved searches, but we want to keep them separate because they need
300299
// to be applied _first_ because other saved objects can be dependent on those saved searches existing
@@ -311,7 +310,7 @@ export async function resolveSavedObjects(
311310
// likely that these saved objects will work once resaved so keep them around to resave them.
312311
const conflictedSavedObjectsLinkedToSavedSearches: any[] = [];
313312

314-
await awaitEachItemInParallel(docTypes.searches, async (searchDoc) => {
313+
for (const searchDoc of docTypes.searches) {
315314
const obj = await getSavedObject(searchDoc, services);
316315

317316
try {
@@ -329,9 +328,9 @@ export async function resolveSavedObjects(
329328
failedImports.push({ obj, error });
330329
}
331330
}
332-
});
331+
}
333332

334-
await awaitEachItemInParallel(docTypes.other, async (otherDoc) => {
333+
for (const otherDoc of docTypes.other) {
335334
const obj = await getSavedObject(otherDoc, services);
336335

337336
try {
@@ -350,7 +349,7 @@ export async function resolveSavedObjects(
350349
failedImports.push({ obj, error });
351350
}
352351
}
353-
});
352+
}
354353

355354
return {
356355
conflictedIndexPatterns,

test/functional/apps/management/_import_objects.js

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import expect from '@kbn/expect';
2121
import path from 'path';
2222
import { keyBy } from 'lodash';
2323

24+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
25+
2426
export default function ({ getService, getPageObjects }) {
2527
const kibanaServer = getService('kibanaServer');
2628
const esArchiver = getService('esArchiver');
@@ -203,12 +205,12 @@ export default function ({ getService, getPageObjects }) {
203205
// delete .kibana index and then wait for Kibana to re-create it
204206
await kibanaServer.uiSettings.replace({});
205207
await PageObjects.settings.navigateTo();
206-
await esArchiver.load('management');
208+
await esArchiver.load('saved_objects_imports');
207209
await PageObjects.settings.clickKibanaSavedObjects();
208210
});
209211

210212
afterEach(async function () {
211-
await esArchiver.unload('management');
213+
await esArchiver.unload('saved_objects_imports');
212214
});
213215

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

285+
it('should allow the user to confirm overriding multiple duplicate saved objects', async function () {
286+
// This data has already been loaded by the "visualize" esArchive. We'll load it again
287+
// so that we can override the existing visualization.
288+
await PageObjects.savedObjects.importFile(
289+
path.join(__dirname, 'exports', '_import_objects_multiple_exists.json'),
290+
false
291+
);
292+
293+
await PageObjects.savedObjects.checkImportLegacyWarning();
294+
await PageObjects.savedObjects.checkImportConflictsWarning();
295+
296+
await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
297+
await PageObjects.savedObjects.clickConfirmChanges();
298+
299+
// Override the visualizations.
300+
await PageObjects.common.clickConfirmOnModal(false);
301+
// as the second confirm can pop instantly, we can't wait for it to be hidden
302+
// with is why we call clickConfirmOnModal with ensureHidden: false in previous statement
303+
// but as the initial popin can take a few ms before fading, we need to wait a little
304+
// to avoid clicking twice on the same modal.
305+
await delay(1000);
306+
await PageObjects.common.clickConfirmOnModal(false);
307+
308+
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
309+
expect(isSuccessful).to.be(true);
310+
});
311+
312+
it('should allow the user to confirm overriding multiple duplicate index patterns', async function () {
313+
// This data has already been loaded by the "visualize" esArchive. We'll load it again
314+
// so that we can override the existing visualization.
315+
await PageObjects.savedObjects.importFile(
316+
path.join(__dirname, 'exports', '_import_index_patterns_multiple_exists.json'),
317+
false
318+
);
319+
320+
// Override the index patterns.
321+
await PageObjects.common.clickConfirmOnModal(false);
322+
// as the second confirm can pop instantly, we can't wait for it to be hidden
323+
// with is why we call clickConfirmOnModal with ensureHidden: false in previous statement
324+
// but as the initial popin can take a few ms before fading, we need to wait a little
325+
// to avoid clicking twice on the same modal.
326+
await delay(1000);
327+
await PageObjects.common.clickConfirmOnModal(false);
328+
329+
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
330+
expect(isSuccessful).to.be(true);
331+
});
332+
283333
it('should import saved objects linked to saved searches', async function () {
284334
await PageObjects.savedObjects.importFile(
285335
path.join(__dirname, 'exports', '_import_objects_saved_search.json')

test/functional/apps/management/exports/_import_index_patterns_multiple_exists.json

Lines changed: 26 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[
2+
{
3+
"_id": "test-1",
4+
"_type": "visualization",
5+
"_source": {
6+
"title": "Visualization test 1",
7+
"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\":{}}",
8+
"uiStateJSON": "{}",
9+
"description": "AreaChart",
10+
"version": 1,
11+
"kibanaSavedObjectMeta": {
12+
"searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
13+
}
14+
},
15+
"_meta": {
16+
"savedObjectVersion": 2
17+
}
18+
},
19+
{
20+
"_id": "test-2",
21+
"_type": "visualization",
22+
"_source": {
23+
"title": "Visualization test 2",
24+
"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\":{}}",
25+
"uiStateJSON": "{}",
26+
"description": "AreaChart",
27+
"version": 1,
28+
"kibanaSavedObjectMeta": {
29+
"searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
30+
}
31+
},
32+
"_meta": {
33+
"savedObjectVersion": 2
34+
}
35+
}
36+
]
Binary file not shown.

0 commit comments

Comments
 (0)