Resolve filter index references when importing saved objects (#42974) (#44482)

* Resolve filter index references when importing saved objects

* Tests for filter index resolution

* saved_object.js test for serialization of non-existing searchSource indexes
This commit is contained in:
Rudolf Meijering 2019-08-30 17:32:46 +02:00 committed by GitHub
parent c0c922355c
commit a10c93ec5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 12 deletions

View file

@ -435,6 +435,17 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
],
"newIndexPatternId": undefined,
},
Object {
"existingIndexPatternId": "filterIndex",
"list": Array [
Object {
"id": "filterIndex",
"title": "MyIndexPattern*",
"type": "index-pattern",
},
],
"newIndexPatternId": undefined,
},
]
}
pagination={

View file

@ -418,8 +418,12 @@ describe('Flyout', () => {
},
obj: {
searchSource: {
getOwnField: () => 'MyIndexPattern*',
getOwnField: (field) => {
if(field === 'index') { return 'MyIndexPattern*';}
if(field === 'filter') { return [{ meta: { index: 'filterIndex' } }];}
},
},
_serialize: () => { return { references: [{ id: 'MyIndexPattern*' }, { id: 'filterIndex' }] };},
},
},
];
@ -477,7 +481,17 @@ describe('Flyout', () => {
type: 'index-pattern',
},
],
},
}, {
'existingIndexPatternId': 'filterIndex',
'list': [
{
'id': 'filterIndex',
'title': 'MyIndexPattern*',
'type': 'index-pattern',
},
],
'newIndexPatternId': undefined,
}
],
});
});

View file

@ -19,7 +19,7 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { groupBy, take, get as getField } from 'lodash';
import { take, get as getField } from 'lodash';
import {
EuiFlyout,
EuiFlyoutBody,
@ -273,9 +273,15 @@ class FlyoutUI extends Component {
confirmModalPromise
);
const byId = groupBy(conflictedIndexPatterns, ({ obj }) =>
obj.searchSource.getOwnField('index')
);
const byId = {};
conflictedIndexPatterns
.map(({ doc, obj }) => {
return { doc, obj: obj._serialize() };
}).forEach(({ doc, obj }) =>
obj.references.forEach(ref => {
byId[ref.id] = byId[ref.id] != null ? byId[ref.id].concat({ doc, obj }) : [{ doc, obj }];
})
);
const unmatchedReferences = Object.entries(byId).reduce(
(accum, [existingIndexPatternId, list]) => {
accum.push({

View file

@ -238,7 +238,9 @@ describe('resolveSavedObjects', () => {
{
obj: {
searchSource: {
getOwnField: () => '1',
getOwnField: (field) => {
return field === 'index' ? '1' : undefined;
},
},
hydrateIndexPattern,
save,
@ -247,7 +249,9 @@ describe('resolveSavedObjects', () => {
{
obj: {
searchSource: {
getOwnField: () => '3',
getOwnField: (field) => {
return field === 'index' ? '3' : undefined;
},
},
hydrateIndexPattern,
save,
@ -283,6 +287,63 @@ describe('resolveSavedObjects', () => {
expect(hydrateIndexPattern).toHaveBeenCalledWith('2');
expect(hydrateIndexPattern).toHaveBeenCalledWith('4');
});
it('should resolve filter index conflicts', async () => {
const hydrateIndexPattern = jest.fn();
const save = jest.fn();
const conflictedIndexPatterns = [
{
obj: {
searchSource: {
getOwnField: (field) => {
return field === 'index' ? '1' : [{ meta: { index: 'filterIndex' } }];
},
setField: jest.fn(),
},
hydrateIndexPattern,
save,
},
},
{
obj: {
searchSource: {
getOwnField: (field) => {
return field === 'index' ? '3' : undefined;
},
},
hydrateIndexPattern,
save,
},
},
];
const resolutions = [
{
oldId: '1',
newId: '2',
},
{
oldId: '3',
newId: '4',
},
{
oldId: 'filterIndex',
newId: 'newFilterIndex',
},
];
const overwriteAll = false;
await resolveIndexPatternConflicts(
resolutions,
conflictedIndexPatterns,
overwriteAll
);
expect(conflictedIndexPatterns[0].obj.searchSource.setField).toHaveBeenCalledWith('filter', [{ meta: { index: 'newFilterIndex' } }]);
expect(save.mock.calls.length).toBe(2);
});
});
describe('saveObjects', () => {

View file

@ -140,19 +140,38 @@ export async function resolveIndexPatternConflicts(
overwriteAll
) {
let importCount = 0;
await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj }) => {
// Resolve search index reference:
let oldIndexId = obj.searchSource.getOwnField('index');
// Depending on the object, this can either be the raw id or the actual index pattern object
if (typeof oldIndexId !== 'string') {
oldIndexId = oldIndexId.id;
}
const resolution = resolutions.find(({ oldId }) => oldId === oldIndexId);
let resolution = resolutions.find(({ oldId }) => oldId === oldIndexId);
if (resolution) {
const newIndexId = resolution.newId;
await obj.hydrateIndexPattern(newIndexId);
}
// Resolve filter index reference:
const filter = (obj.searchSource.getOwnField('filter') || []).map((filter) => {
if (!(filter.meta && filter.meta.index)) {
return filter;
}
resolution = resolutions.find(({ oldId }) => oldId === filter.meta.index);
return resolution ? ({ ...filter, ...{ meta: { ...filter.meta, index: resolution.newId } } }) : filter;
});
if (filter.length > 0) {
obj.searchSource.setField('filter', filter);
}
if (!resolution) {
// The user decided to skip this conflict so do nothing
return;
}
const newIndexId = resolution.newId;
await obj.hydrateIndexPattern(newIndexId);
if (await saveObject(obj, overwriteAll)) {
importCount++;
}

View file

@ -361,6 +361,40 @@ describe('Saved Object', function () {
});
});
it('when index in searchSourceJSON is not found', () => {
const id = '123';
stubESResponse(getMockedDocResponse(id));
return createInitializedSavedObject({ type: 'dashboard', searchSource: true })
.then((savedObject) => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.resolve({
id,
version: 2,
type: 'dashboard',
});
});
savedObject.searchSource.setFields({ 'index': 'non-existant-index' });
return savedObject
.save()
.then(() => {
expect(savedObjectsClientStub.create.getCall(0).args[1]).to.eql({
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
}),
},
});
const { references } = savedObjectsClientStub.create.getCall(0).args[2];
expect(references).to.have.length(1);
expect(references[0]).to.eql({
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
id: 'non-existant-index',
});
});
});
});
it('when indexes exists in filter of searchSourceJSON', () => {
const id = '123';
stubESResponse(getMockedDocResponse(id));

View file

@ -321,7 +321,10 @@ export function SavedObjectProvider(Promise, Private, confirmModalPromise, index
if (this.searchSource) {
let searchSourceFields = _.omit(this.searchSource.getFields(), ['sort', 'size']);
if (searchSourceFields.index) {
const { id: indexId } = searchSourceFields.index;
// searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios:
// (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on this Saved Object
// (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()`
const indexId = typeof (searchSourceFields.index) === 'string' ? searchSourceFields.index : searchSourceFields.index.id;
const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index';
references.push({
name: refName,