Clean up client-side saved object data loading infrastructure (#30241)

This commit is contained in:
Joe Reuter 2019-02-19 10:45:44 +01:00 committed by GitHub
parent 97684d176e
commit 6b0fa77fcb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 957 additions and 1242 deletions

View file

@ -18,13 +18,13 @@
*/
import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields';
import { SavedObject } from 'ui/saved_objects';
import { SimpleSavedObject } from 'ui/saved_objects';
export function FixturesStubbedSavedObjectIndexPatternProvider(Private) {
const mockLogstashFields = Private(FixturesLogstashFieldsProvider);
return function (id) {
return new SavedObject(undefined, {
return new SimpleSavedObject(undefined, {
id,
type: 'index-pattern',
attributes: {

View file

@ -21,7 +21,7 @@ import angular from 'angular';
import { uiModules } from 'ui/modules';
import { createDashboardEditUrl } from '../dashboard_constants';
import { createLegacyClass } from 'ui/utils/legacy_class';
import { SavedObjectProvider } from 'ui/courier';
import { SavedObjectProvider } from 'ui/saved_objects/saved_object';
import {
extractReferences,
injectReferences,

View file

@ -20,9 +20,8 @@
import { i18n } from '@kbn/i18n';
import './saved_dashboard';
import { uiModules } from 'ui/modules';
import { SavedObjectLoader } from 'ui/courier/saved_object/saved_object_loader';
import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects';
import { savedObjectManagementRegistry } from '../../management/saved_object_registry';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
const module = uiModules.get('app/dashboard');
@ -38,5 +37,5 @@ savedObjectManagementRegistry.register({
// This is the only thing that gets injected into controllers
module.service('savedDashboards', function (Private, SavedDashboard, kbnIndex, kbnUrl, $http, chrome) {
const savedObjectClient = Private(SavedObjectsClientProvider);
return new SavedObjectLoader(SavedDashboard, kbnIndex, kbnUrl, $http, chrome, savedObjectClient);
return new SavedObjectLoader(SavedDashboard, kbnUrl, chrome, savedObjectClient);
});

View file

@ -27,7 +27,7 @@ import 'ui/private';
import '../../components/field_chooser/field_chooser';
import FixturesHitsProvider from 'fixtures/hits';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { SavedObject } from 'ui/saved_objects';
import { SimpleSavedObject } from 'ui/saved_objects';
// Load the kibana app dependencies.
@ -91,9 +91,9 @@ describe('discover field chooser directives', function () {
hits = Private(FixturesHitsProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
indexPatternList = [
new SavedObject(undefined, { id: '0', attributes: { title: 'b' } }),
new SavedObject(undefined, { id: '1', attributes: { title: 'a' } }),
new SavedObject(undefined, { id: '2', attributes: { title: 'c' } })
new SimpleSavedObject(undefined, { id: '0', attributes: { title: 'b' } }),
new SimpleSavedObject(undefined, { id: '1', attributes: { title: 'a' } }),
new SimpleSavedObject(undefined, { id: '2', attributes: { title: 'c' } })
];
const fieldCounts = _.transform(hits, function (counts, hit) {

View file

@ -20,7 +20,7 @@
import 'ui/notify';
import { uiModules } from 'ui/modules';
import { createLegacyClass } from 'ui/utils/legacy_class';
import { SavedObjectProvider } from 'ui/courier';
import { SavedObjectProvider } from 'ui/saved_objects/saved_object';
const module = uiModules.get('discover/saved_searches', [
'kibana/notify',

View file

@ -20,9 +20,8 @@
import './_saved_search';
import 'ui/notify';
import { uiModules } from 'ui/modules';
import { SavedObjectLoader } from 'ui/courier/saved_object/saved_object_loader';
import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects';
import { savedObjectManagementRegistry } from '../../management/saved_object_registry';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
const module = uiModules.get('discover/saved_searches', [
'kibana/notify'
]);
@ -36,7 +35,7 @@ savedObjectManagementRegistry.register({
module.service('savedSearches', function (Private, Promise, config, kbnIndex, createNotifier, SavedSearch, kbnUrl, $http, chrome) {
const savedObjectClient = Private(SavedObjectsClientProvider);
const savedSearchLoader = new SavedObjectLoader(SavedSearch, kbnIndex, kbnUrl, $http, chrome, savedObjectClient);
const savedSearchLoader = new SavedObjectLoader(SavedSearch, kbnUrl, chrome, savedObjectClient);
// Customize loader properties since adding an 's' on type doesn't work for type 'search' .
savedSearchLoader.loaderProperties = {
name: 'searches',

View file

@ -30,7 +30,7 @@ import { uiModules } from 'ui/modules';
import { updateOldState } from 'ui/vis/vis_update_state';
import { VisualizeConstants } from '../visualize_constants';
import { createLegacyClass } from 'ui/utils/legacy_class';
import { SavedObjectProvider } from 'ui/courier';
import { SavedObjectProvider } from 'ui/saved_objects/saved_object';
import {
extractReferences,
injectReferences,

View file

@ -20,9 +20,8 @@
import './_saved_vis';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { uiModules } from 'ui/modules';
import { SavedObjectLoader } from 'ui/courier/saved_object/saved_object_loader';
import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects';
import { savedObjectManagementRegistry } from '../../management/saved_object_registry';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
const app = uiModules.get('app/visualize');
@ -37,7 +36,7 @@ app.service('savedVisualizations', function (Promise, kbnIndex, SavedVis, Privat
const visTypes = Private(VisTypesRegistryProvider);
const savedObjectClient = Private(SavedObjectsClientProvider);
const saveVisualizationLoader = new SavedObjectLoader(SavedVis, kbnIndex, kbnUrl, $http, chrome, savedObjectClient);
const saveVisualizationLoader = new SavedObjectLoader(SavedVis, kbnUrl, chrome, savedObjectClient);
saveVisualizationLoader.mapHitSource = function (source, id) {
source.id = id;

View file

@ -19,7 +19,7 @@
import { uiModules } from 'ui/modules';
import { createLegacyClass } from 'ui/utils/legacy_class';
import { SavedObjectProvider } from 'ui/courier';
import { SavedObjectProvider } from 'ui/saved_objects/saved_object';
const module = uiModules.get('app/timelion');
// Used only by the savedSheets service, usually no reason to change this

View file

@ -17,9 +17,8 @@
* under the License.
*/
import { SavedObjectLoader } from 'ui/courier/saved_object/saved_object_loader';
import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects';
import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_object_registry';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { uiModules } from 'ui/modules';
import './_saved_sheet.js';
@ -35,7 +34,7 @@ savedObjectManagementRegistry.register({
// This is the only thing that gets injected into controllers
module.service('savedSheets', function (Private, Promise, SavedSheet, kbnIndex, kbnUrl, $http, chrome) {
const savedObjectClient = Private(SavedObjectsClientProvider);
const savedSheetLoader = new SavedObjectLoader(SavedSheet, kbnIndex, kbnUrl, $http, chrome, savedObjectClient);
const savedSheetLoader = new SavedObjectLoader(SavedSheet, kbnUrl, chrome, savedObjectClient);
savedSheetLoader.urlFor = function (id) {
return kbnUrl.eval('#/{{id}}', { id: id });
};

View file

@ -50,7 +50,7 @@ import '../tooltip';
import '../url';
import '../validate_date_interval';
import '../watch_multi';
import '../courier/saved_object/ui/saved_object_save_as_checkbox';
import '../saved_objects/ui/saved_object_save_as_checkbox';
import '../react_components';
import '../i18n';
import '../query_bar/directive';

View file

@ -19,7 +19,6 @@
import './courier';
export { SavedObjectProvider } from './saved_object';
export { SearchSourceProvider } from './search_source';
export {

View file

@ -1,823 +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 ngMock from 'ng_mock';
import expect from 'expect.js';
import sinon from 'sinon';
import BluebirdPromise from 'bluebird';
import { SavedObjectProvider } from '../saved_object';
import { IndexPatternProvider } from '../../../index_patterns/_index_pattern';
import { SavedObjectsClientProvider } from '../../../saved_objects';
import { StubIndexPatternsApiClientModule } from '../../../index_patterns/__tests__/stub_index_patterns_api_client';
import { InvalidJSONProperty } from '../../../errors';
describe('Saved Object', function () {
require('test_utils/no_digest_promises').activateForSuite();
let SavedObject;
let IndexPattern;
let esDataStub;
let savedObjectsClientStub;
let window;
/**
* Returns a fake doc response with the given index and id, of type dashboard
* that can be used to stub es calls.
* @param indexPatternId
* @param additionalOptions - object that will be assigned to the mocked doc response.
* @returns {{attributes: {}, type: string, id: *, _version: string}}
*/
function getMockedDocResponse(indexPatternId, additionalOptions = {}) {
return {
type: 'dashboard',
id: indexPatternId,
_version: 'foo',
attributes: {},
...additionalOptions
};
}
/**
* Stubs some of the es retrieval calls so it returns the given response.
* @param {Object} mockDocResponse
*/
function stubESResponse(mockDocResponse) {
// Stub out search for duplicate title:
sinon.stub(savedObjectsClientStub, 'get').returns(BluebirdPromise.resolve(mockDocResponse));
sinon.stub(savedObjectsClientStub, 'update').returns(BluebirdPromise.resolve(mockDocResponse));
sinon.stub(savedObjectsClientStub, 'find').returns(BluebirdPromise.resolve({ savedObjects: [], total: 0 }));
sinon.stub(savedObjectsClientStub, 'bulkGet').returns(BluebirdPromise.resolve({ savedObjects: [mockDocResponse] }));
}
/**
* Creates a new saved object with the given configuration and initializes it.
* Returns the promise that will be completed when the initialization finishes.
*
* @param {Object} config
* @returns {Promise<SavedObject>} A promise that resolves with an instance of
* SavedObject
*/
function createInitializedSavedObject(config = {}) {
const savedObject = new SavedObject(config);
savedObject.title = 'my saved object';
return savedObject.init();
}
const mock409FetchError = {
res: { status: 409 }
};
beforeEach(ngMock.module(
'kibana',
StubIndexPatternsApiClientModule,
// Use the native window.confirm instead of our specialized version to make testing
// this easier.
function ($provide) {
const overrideConfirm = message => window.confirm(message) ? Promise.resolve() : Promise.reject();
$provide.decorator('confirmModalPromise', () => overrideConfirm);
})
);
beforeEach(ngMock.inject(function (es, Private, $window) {
SavedObject = Private(SavedObjectProvider);
IndexPattern = Private(IndexPatternProvider);
esDataStub = es;
savedObjectsClientStub = Private(SavedObjectsClientProvider);
window = $window;
}));
describe('save', function () {
describe('with confirmOverwrite', function () {
function stubConfirmOverwrite() {
window.confirm = sinon.stub().returns(true);
sinon.stub(esDataStub, 'create').returns(BluebirdPromise.reject(mock409FetchError));
}
describe('when true', function () {
it('requests confirmation and updates on yes response', function () {
stubESResponse(getMockedDocResponse('myId'));
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
const createStub = sinon.stub(savedObjectsClientStub, 'create');
createStub.onFirstCall().returns(BluebirdPromise.reject(mock409FetchError));
createStub.onSecondCall().returns(BluebirdPromise.resolve({ id: 'myId' }));
stubConfirmOverwrite();
savedObject.lastSavedTitle = 'original title';
savedObject.title = 'new title';
return savedObject.save({ confirmOverwrite: true })
.then(() => {
expect(window.confirm.called).to.be(true);
expect(savedObject.id).to.be('myId');
expect(savedObject.isSaving).to.be(false);
expect(savedObject.lastSavedTitle).to.be('new title');
expect(savedObject.title).to.be('new title');
});
});
});
it('does not update on no response', function () {
stubESResponse(getMockedDocResponse('HI'));
return createInitializedSavedObject({ type: 'dashboard', id: 'HI' }).then(savedObject => {
window.confirm = sinon.stub().returns(false);
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.reject(mock409FetchError));
savedObject.lastSavedTitle = 'original title';
savedObject.title = 'new title';
return savedObject.save({ confirmOverwrite: true })
.then(() => {
expect(savedObject.id).to.be('HI');
expect(savedObject.isSaving).to.be(false);
expect(savedObject.lastSavedTitle).to.be('original title');
expect(savedObject.title).to.be('new title');
});
});
});
it('handles create failures', function () {
stubESResponse(getMockedDocResponse('myId'));
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
stubConfirmOverwrite();
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.reject(mock409FetchError));
return savedObject.save({ confirmOverwrite: true })
.then(() => {
expect(true).to.be(false); // Force failure, the save should not succeed.
})
.catch(() => {
expect(window.confirm.called).to.be(true);
});
});
});
});
it('when false does not request overwrite', function () {
const mockDocResponse = getMockedDocResponse('myId');
stubESResponse(mockDocResponse);
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
stubConfirmOverwrite();
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.resolve({ id: 'myId' }));
return savedObject.save({ confirmOverwrite: false }).then(() => {
expect(window.confirm.called).to.be(false);
});
});
});
});
describe('with copyOnSave', function () {
it('as true creates a copy on save success', function () {
const mockDocResponse = getMockedDocResponse('myId');
stubESResponse(mockDocResponse);
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.resolve({ type: 'dashboard', id: 'newUniqueId' });
});
savedObject.copyOnSave = true;
return savedObject.save().then((id) => {
expect(id).to.be('newUniqueId');
});
});
});
it('as true does not create a copy when save fails', function () {
const originalId = 'id1';
const mockDocResponse = getMockedDocResponse(originalId);
stubESResponse(mockDocResponse);
return createInitializedSavedObject({ type: 'dashboard', id: originalId }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.reject('simulated error');
});
savedObject.copyOnSave = true;
return savedObject.save().then(() => {
throw new Error('Expected a rejection');
}).catch(() => {
expect(savedObject.id).to.be(originalId);
});
});
});
it('as false does not create a copy', function () {
const id = 'myId';
const mockDocResponse = getMockedDocResponse(id);
stubESResponse(mockDocResponse);
return createInitializedSavedObject({ type: 'dashboard', id: id }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
expect(savedObject.id).to.be(id);
return BluebirdPromise.resolve(id);
});
savedObject.copyOnSave = false;
return savedObject.save().then((id) => {
expect(id).to.be(id);
});
});
});
});
it('returns id from server on success', function () {
return createInitializedSavedObject({ type: 'dashboard' }).then(savedObject => {
const mockDocResponse = getMockedDocResponse('myId');
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.resolve({
type: 'dashboard',
id: 'myId',
_version: 'foo'
});
});
stubESResponse(mockDocResponse);
return savedObject.save().then(id => {
expect(id).to.be('myId');
});
});
});
describe('updates isSaving variable', function () {
it('on success', function () {
const id = 'id';
stubESResponse(getMockedDocResponse(id));
return createInitializedSavedObject({ type: 'dashboard', id: id }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
expect(savedObject.isSaving).to.be(true);
return BluebirdPromise.resolve({
type: 'dashboard',
id,
version: 'foo'
});
});
expect(savedObject.isSaving).to.be(false);
return savedObject.save().then(() => {
expect(savedObject.isSaving).to.be(false);
});
});
});
it('on failure', function () {
stubESResponse(getMockedDocResponse('id'));
return createInitializedSavedObject({ type: 'dashboard' }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
expect(savedObject.isSaving).to.be(true);
return BluebirdPromise.reject();
});
expect(savedObject.isSaving).to.be(false);
return savedObject.save().catch(() => {
expect(savedObject.isSaving).to.be(false);
});
});
});
});
describe('to extract references', () => {
it('when "extractReferences" function when passed in', async () => {
const id = '123';
stubESResponse(getMockedDocResponse(id));
const extractReferences = ({ attributes, references }) => {
references.push({
name: 'test',
type: 'index-pattern',
id: 'my-index',
});
return { attributes, references };
};
return createInitializedSavedObject({ type: 'dashboard', extractReferences })
.then((savedObject) => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.resolve({
id,
version: 'foo',
type: 'dashboard',
});
});
return savedObject
.save()
.then(() => {
const { references } = savedObjectsClientStub.create.getCall(0).args[2];
expect(references).to.have.length(1);
expect(references[0]).to.eql({
name: 'test',
type: 'index-pattern',
id: 'my-index',
});
});
});
});
it('when index exists in searchSourceJSON', () => {
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.setField('index', new IndexPattern('my-index', null, []));
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: 'my-index',
});
});
});
});
it('when indexes exists in filter of searchSourceJSON', () => {
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.setField('filter', [{
meta: {
index: 'my-index',
}
}]);
return savedObject
.save()
.then(() => {
expect(savedObjectsClientStub.create.getCall(0).args[1]).to.eql({
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
filter: [
{
meta: {
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
}
}
],
}),
},
});
const { references } = savedObjectsClientStub.create.getCall(0).args[2];
expect(references).to.have.length(1);
expect(references[0]).to.eql({
name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
type: 'index-pattern',
id: 'my-index',
});
});
});
});
});
});
describe('applyESResp', function () {
it('throws error if not found', function () {
return createInitializedSavedObject({ type: 'dashboard' }).then(savedObject => {
const response = {};
try {
savedObject.applyESResp(response);
expect(true).to.be(false);
} catch (err) {
expect(!!err).to.be(true);
}
});
});
it('throws error invalid JSON is detected', async function () {
const savedObject = await createInitializedSavedObject({ type: 'dashboard', searchSource: true });
const response = {
found: true,
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: '\"{\\n \\\"filter\\\": []\\n}\"'
}
}
};
try {
await savedObject.applyESResp(response);
throw new Error('applyESResp should have failed, but did not.');
} catch (err) {
expect(err instanceof InvalidJSONProperty).to.be(true);
}
});
it('preserves original defaults if not overridden', function () {
const id = 'anid';
const preserveMeValue = 'here to stay!';
const config = {
defaults: {
preserveMe: preserveMeValue
},
type: 'dashboard',
id: id
};
const mockDocResponse = getMockedDocResponse(id);
stubESResponse(mockDocResponse);
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
expect(savedObject._source.preserveMe).to.equal(preserveMeValue);
const response = { found: true, _source: {} };
return savedObject.applyESResp(response);
}).then(() => {
expect(savedObject._source.preserveMe).to.equal(preserveMeValue);
});
});
it('overrides defaults', function () {
const id = 'anid';
const config = {
defaults: {
flower: 'rose'
},
type: 'dashboard',
id: id
};
const mockDocResponse = getMockedDocResponse(id);
stubESResponse(mockDocResponse);
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
expect(savedObject._source.flower).to.equal('rose');
const response = {
found: true,
_source: {
flower: 'orchid'
}
};
return savedObject.applyESResp(response);
}).then(() => {
expect(savedObject._source.flower).to.equal('orchid');
});
});
it('overrides previous _source and default values', function () {
const id = 'anid';
const config = {
defaults: {
dinosaurs: {
tRex: 'is the scariest'
}
},
type: 'dashboard',
id: id
};
const mockDocResponse = getMockedDocResponse(
id,
{ attributes: { dinosaurs: { tRex: 'is not so bad' }, } });
stubESResponse(mockDocResponse);
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
const response = {
found: true,
_source: { dinosaurs: { tRex: 'has big teeth' } }
};
return savedObject.applyESResp(response);
})
.then(() => {
expect(savedObject._source.dinosaurs.tRex).to.equal('has big teeth');
});
});
it('does not inject references when references array is missing', async () => {
const injectReferences = sinon.stub();
const config = {
type: 'dashboard',
injectReferences,
};
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
const response = {
found: true,
_source: {
dinosaurs: { tRex: 'has big teeth' },
},
};
return savedObject.applyESResp(response);
})
.then(() => {
expect(injectReferences).to.have.property('notCalled', true);
});
});
it('does not inject references when references array is empty', async () => {
const injectReferences = sinon.stub();
const config = {
type: 'dashboard',
injectReferences,
};
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
const response = {
found: true,
_source: {
dinosaurs: { tRex: 'has big teeth' },
},
references: [],
};
return savedObject.applyESResp(response);
})
.then(() => {
expect(injectReferences).to.have.property('notCalled', true);
});
});
it('injects references when function is provided and references exist', async () => {
const injectReferences = sinon.stub();
const config = {
type: 'dashboard',
injectReferences,
};
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
const response = {
found: true,
_source: {
dinosaurs: { tRex: 'has big teeth' },
},
references: [{}],
};
return savedObject.applyESResp(response);
})
.then(() => {
expect(injectReferences).to.have.property('calledOnce', true);
});
});
it('injects references from searchSourceJSON', async () => {
const savedObject = new SavedObject({ type: 'dashboard', searchSource: true });
return savedObject
.init()
.then(() => {
const response = {
found: true,
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
filter: [
{
meta: {
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
},
},
],
}),
},
},
references: [
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
id: 'my-index-1',
},
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
type: 'index-pattern',
id: 'my-index-2',
},
],
};
savedObject.applyESResp(response);
expect(savedObject.searchSource.getFields()).to.eql({
index: 'my-index-1',
filter: [
{
meta: {
index: 'my-index-2',
},
},
],
});
});
});
});
describe ('config', function () {
it('afterESResp is called', function () {
const afterESRespCallback = sinon.spy();
const config = {
type: 'dashboard',
afterESResp: afterESRespCallback
};
return createInitializedSavedObject(config).then(() => {
expect(afterESRespCallback.called).to.be(true);
});
});
it('init is called', function () {
const initCallback = sinon.spy();
const config = {
type: 'dashboard',
init: initCallback
};
return createInitializedSavedObject(config).then(() => {
expect(initCallback.called).to.be(true);
});
});
describe('searchSource', function () {
it('when true, creates index', function () {
const indexPatternId = 'testIndexPattern';
const afterESRespCallback = sinon.spy();
const config = {
type: 'dashboard',
afterESResp: afterESRespCallback,
searchSource: true,
indexPattern: indexPatternId
};
stubESResponse({
id: indexPatternId,
type: 'dashboard',
attributes: {
title: 'testIndexPattern'
},
_version: 'foo'
});
const savedObject = new SavedObject(config);
expect(!!savedObject.searchSource.getField('index')).to.be(false);
return savedObject.init().then(() => {
expect(afterESRespCallback.called).to.be(true);
const index = savedObject.searchSource.getField('index');
expect(index instanceof IndexPattern).to.be(true);
expect(index.id).to.equal(indexPatternId);
});
});
it('when false, does not create index', function () {
const indexPatternId = 'testIndexPattern';
const afterESRespCallback = sinon.spy();
const config = {
type: 'dashboard',
afterESResp: afterESRespCallback,
searchSource: false,
indexPattern: indexPatternId
};
stubESResponse(getMockedDocResponse(indexPatternId));
const savedObject = new SavedObject(config);
expect(!!savedObject.searchSource).to.be(false);
return savedObject.init().then(() => {
expect(afterESRespCallback.called).to.be(true);
expect(!!savedObject.searchSource).to.be(false);
});
});
});
describe('type', function () {
it('that is not specified throws an error', function () {
const config = {};
const savedObject = new SavedObject(config);
try {
savedObject.init();
expect(false).to.be(true);
} catch (err) {
expect(err).to.not.be(null);
}
});
it('that is invalid invalid throws an error', function () {
const config = { type: 'notypeexists' };
const savedObject = new SavedObject(config);
try {
savedObject.init();
expect(false).to.be(true);
} catch (err) {
expect(err).to.not.be(null);
}
});
it('that is valid passes', function () {
const config = { type: 'dashboard' };
return new SavedObject(config).init();
});
});
describe('defaults', function () {
function getTestDefaultConfig(extraOptions) {
return {
defaults: { testDefault: 'hi' },
type: 'dashboard',
...extraOptions
};
}
function expectDefaultApplied(config) {
return createInitializedSavedObject(config).then((savedObject) => {
expect(savedObject.defaults).to.be(config.defaults);
});
}
describe('applied to object when id', function () {
it('is not specified', function () {
expectDefaultApplied(getTestDefaultConfig());
});
it('is undefined', function () {
expectDefaultApplied(getTestDefaultConfig({ id: undefined }));
});
it('is 0', function () {
expectDefaultApplied(getTestDefaultConfig({ id: 0 }));
});
it('is false', function () {
expectDefaultApplied(getTestDefaultConfig({ id: false }));
});
});
it('applied to source if an id is given', function () {
const myId = 'myid';
const customDefault = 'hi';
const initialOverwriteMeValue = 'this should get overwritten by the server response';
const config = {
defaults: {
overwriteMe: initialOverwriteMeValue,
customDefault: customDefault
},
type: 'dashboard',
id: myId
};
const serverValue = 'this should override the initial default value given';
const mockDocResponse = getMockedDocResponse(
myId,
{ attributes: { overwriteMe: serverValue } });
stubESResponse(mockDocResponse);
return createInitializedSavedObject(config).then((savedObject) => {
expect(!!savedObject._source).to.be(true);
expect(savedObject.defaults).to.be(config.defaults);
expect(savedObject._source.overwriteMe).to.be(serverValue);
expect(savedObject._source.customDefault).to.be(customDefault);
});
});
});
});
});

View file

@ -1,20 +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.
*/
export { SavedObjectProvider } from './saved_object';

View file

@ -20,7 +20,7 @@
import sinon from 'sinon';
import expect from 'expect.js';
import { findObjectByTitle } from '../find_object_by_title';
import { SavedObject } from '../saved_object';
import { SimpleSavedObject } from '../simple_saved_object';
describe('findObjectByTitle', () => {
const sandbox = sinon.createSandbox();
@ -38,7 +38,7 @@ describe('findObjectByTitle', () => {
});
it('matches any case', async () => {
const indexPattern = new SavedObject(savedObjectsClient, { attributes: { title: 'foo' } });
const indexPattern = new SimpleSavedObject(savedObjectsClient, { attributes: { title: 'foo' } });
savedObjectsClient.find.returns(Promise.resolve({
savedObjects: [indexPattern]
}));

View file

@ -17,36 +17,807 @@
* under the License.
*/
import sinon from 'sinon';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import { SavedObject } from '../saved_object';
import sinon from 'sinon';
import BluebirdPromise from 'bluebird';
describe('SavedObject', () => {
it('persists type and id', () => {
const id = 'logstash-*';
const type = 'index-pattern';
import { SavedObjectProvider } from '../saved_object';
import { IndexPatternProvider } from '../../index_patterns/_index_pattern';
import { SavedObjectsClientProvider } from '../saved_objects_client_provider';
import { StubIndexPatternsApiClientModule } from '../../index_patterns/__tests__/stub_index_patterns_api_client';
import { InvalidJSONProperty } from '../../errors';
const client = sinon.stub();
const savedObject = new SavedObject(client, { id, type });
describe('Saved Object', function () {
require('test_utils/no_digest_promises').activateForSuite();
expect(savedObject.id).to.be(id);
expect(savedObject.type).to.be(type);
let SavedObject;
let IndexPattern;
let esDataStub;
let savedObjectsClientStub;
let window;
/**
* Returns a fake doc response with the given index and id, of type dashboard
* that can be used to stub es calls.
* @param indexPatternId
* @param additionalOptions - object that will be assigned to the mocked doc response.
* @returns {{attributes: {}, type: string, id: *, _version: string}}
*/
function getMockedDocResponse(indexPatternId, additionalOptions = {}) {
return {
type: 'dashboard',
id: indexPatternId,
_version: 'foo',
attributes: {},
...additionalOptions
};
}
/**
* Stubs some of the es retrieval calls so it returns the given response.
* @param {Object} mockDocResponse
*/
function stubESResponse(mockDocResponse) {
// Stub out search for duplicate title:
sinon.stub(savedObjectsClientStub, 'get').returns(BluebirdPromise.resolve(mockDocResponse));
sinon.stub(savedObjectsClientStub, 'update').returns(BluebirdPromise.resolve(mockDocResponse));
sinon.stub(savedObjectsClientStub, 'find').returns(BluebirdPromise.resolve({ savedObjects: [], total: 0 }));
sinon.stub(savedObjectsClientStub, 'bulkGet').returns(BluebirdPromise.resolve({ savedObjects: [mockDocResponse] }));
}
/**
* Creates a new saved object with the given configuration and initializes it.
* Returns the promise that will be completed when the initialization finishes.
*
* @param {Object} config
* @returns {Promise<SavedObject>} A promise that resolves with an instance of
* SavedObject
*/
function createInitializedSavedObject(config = {}) {
const savedObject = new SavedObject(config);
savedObject.title = 'my saved object';
return savedObject.init();
}
const mock409FetchError = {
res: { status: 409 }
};
beforeEach(ngMock.module(
'kibana',
StubIndexPatternsApiClientModule,
// Use the native window.confirm instead of our specialized version to make testing
// this easier.
function ($provide) {
const overrideConfirm = message => window.confirm(message) ? Promise.resolve() : Promise.reject();
$provide.decorator('confirmModalPromise', () => overrideConfirm);
})
);
beforeEach(ngMock.inject(function (es, Private, $window) {
SavedObject = Private(SavedObjectProvider);
IndexPattern = Private(IndexPatternProvider);
esDataStub = es;
savedObjectsClientStub = Private(SavedObjectsClientProvider);
window = $window;
}));
describe('save', function () {
describe('with confirmOverwrite', function () {
function stubConfirmOverwrite() {
window.confirm = sinon.stub().returns(true);
sinon.stub(esDataStub, 'create').returns(BluebirdPromise.reject(mock409FetchError));
}
describe('when true', function () {
it('requests confirmation and updates on yes response', function () {
stubESResponse(getMockedDocResponse('myId'));
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
const createStub = sinon.stub(savedObjectsClientStub, 'create');
createStub.onFirstCall().returns(BluebirdPromise.reject(mock409FetchError));
createStub.onSecondCall().returns(BluebirdPromise.resolve({ id: 'myId' }));
stubConfirmOverwrite();
savedObject.lastSavedTitle = 'original title';
savedObject.title = 'new title';
return savedObject.save({ confirmOverwrite: true })
.then(() => {
expect(window.confirm.called).to.be(true);
expect(savedObject.id).to.be('myId');
expect(savedObject.isSaving).to.be(false);
expect(savedObject.lastSavedTitle).to.be('new title');
expect(savedObject.title).to.be('new title');
});
});
});
it('does not update on no response', function () {
stubESResponse(getMockedDocResponse('HI'));
return createInitializedSavedObject({ type: 'dashboard', id: 'HI' }).then(savedObject => {
window.confirm = sinon.stub().returns(false);
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.reject(mock409FetchError));
savedObject.lastSavedTitle = 'original title';
savedObject.title = 'new title';
return savedObject.save({ confirmOverwrite: true })
.then(() => {
expect(savedObject.id).to.be('HI');
expect(savedObject.isSaving).to.be(false);
expect(savedObject.lastSavedTitle).to.be('original title');
expect(savedObject.title).to.be('new title');
});
});
});
it('handles create failures', function () {
stubESResponse(getMockedDocResponse('myId'));
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
stubConfirmOverwrite();
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.reject(mock409FetchError));
return savedObject.save({ confirmOverwrite: true })
.then(() => {
expect(true).to.be(false); // Force failure, the save should not succeed.
})
.catch(() => {
expect(window.confirm.called).to.be(true);
});
});
});
});
it('when false does not request overwrite', function () {
const mockDocResponse = getMockedDocResponse('myId');
stubESResponse(mockDocResponse);
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
stubConfirmOverwrite();
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.resolve({ id: 'myId' }));
return savedObject.save({ confirmOverwrite: false }).then(() => {
expect(window.confirm.called).to.be(false);
});
});
});
});
describe('with copyOnSave', function () {
it('as true creates a copy on save success', function () {
const mockDocResponse = getMockedDocResponse('myId');
stubESResponse(mockDocResponse);
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.resolve({ type: 'dashboard', id: 'newUniqueId' });
});
savedObject.copyOnSave = true;
return savedObject.save().then((id) => {
expect(id).to.be('newUniqueId');
});
});
});
it('as true does not create a copy when save fails', function () {
const originalId = 'id1';
const mockDocResponse = getMockedDocResponse(originalId);
stubESResponse(mockDocResponse);
return createInitializedSavedObject({ type: 'dashboard', id: originalId }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.reject('simulated error');
});
savedObject.copyOnSave = true;
return savedObject.save().then(() => {
throw new Error('Expected a rejection');
}).catch(() => {
expect(savedObject.id).to.be(originalId);
});
});
});
it('as false does not create a copy', function () {
const id = 'myId';
const mockDocResponse = getMockedDocResponse(id);
stubESResponse(mockDocResponse);
return createInitializedSavedObject({ type: 'dashboard', id: id }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
expect(savedObject.id).to.be(id);
return BluebirdPromise.resolve(id);
});
savedObject.copyOnSave = false;
return savedObject.save().then((id) => {
expect(id).to.be(id);
});
});
});
});
it('returns id from server on success', function () {
return createInitializedSavedObject({ type: 'dashboard' }).then(savedObject => {
const mockDocResponse = getMockedDocResponse('myId');
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.resolve({
type: 'dashboard',
id: 'myId',
_version: 'foo'
});
});
stubESResponse(mockDocResponse);
return savedObject.save().then(id => {
expect(id).to.be('myId');
});
});
});
describe('updates isSaving variable', function () {
it('on success', function () {
const id = 'id';
stubESResponse(getMockedDocResponse(id));
return createInitializedSavedObject({ type: 'dashboard', id: id }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
expect(savedObject.isSaving).to.be(true);
return BluebirdPromise.resolve({
type: 'dashboard',
id,
version: 'foo'
});
});
expect(savedObject.isSaving).to.be(false);
return savedObject.save().then(() => {
expect(savedObject.isSaving).to.be(false);
});
});
});
it('on failure', function () {
stubESResponse(getMockedDocResponse('id'));
return createInitializedSavedObject({ type: 'dashboard' }).then(savedObject => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
expect(savedObject.isSaving).to.be(true);
return BluebirdPromise.reject();
});
expect(savedObject.isSaving).to.be(false);
return savedObject.save().catch(() => {
expect(savedObject.isSaving).to.be(false);
});
});
});
});
describe('to extract references', () => {
it('when "extractReferences" function when passed in', async () => {
const id = '123';
stubESResponse(getMockedDocResponse(id));
const extractReferences = ({ attributes, references }) => {
references.push({
name: 'test',
type: 'index-pattern',
id: 'my-index',
});
return { attributes, references };
};
return createInitializedSavedObject({ type: 'dashboard', extractReferences })
.then((savedObject) => {
sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
return BluebirdPromise.resolve({
id,
version: 'foo',
type: 'dashboard',
});
});
return savedObject
.save()
.then(() => {
const { references } = savedObjectsClientStub.create.getCall(0).args[2];
expect(references).to.have.length(1);
expect(references[0]).to.eql({
name: 'test',
type: 'index-pattern',
id: 'my-index',
});
});
});
});
it('when index exists in searchSourceJSON', () => {
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.setField('index', new IndexPattern('my-index', null, []));
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: 'my-index',
});
});
});
});
it('when indexes exists in filter of searchSourceJSON', () => {
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.setField('filter', [{
meta: {
index: 'my-index',
}
}]);
return savedObject
.save()
.then(() => {
expect(savedObjectsClientStub.create.getCall(0).args[1]).to.eql({
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
filter: [
{
meta: {
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
}
}
],
}),
},
});
const { references } = savedObjectsClientStub.create.getCall(0).args[2];
expect(references).to.have.length(1);
expect(references[0]).to.eql({
name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
type: 'index-pattern',
id: 'my-index',
});
});
});
});
});
});
it('persists attributes', () => {
const attributes = { title: 'My title' };
describe('applyESResp', function () {
it('throws error if not found', function () {
return createInitializedSavedObject({ type: 'dashboard' }).then(savedObject => {
const response = {};
try {
savedObject.applyESResp(response);
expect(true).to.be(false);
} catch (err) {
expect(!!err).to.be(true);
}
});
});
const client = sinon.stub();
const savedObject = new SavedObject(client, { attributes });
it('throws error invalid JSON is detected', async function () {
const savedObject = await createInitializedSavedObject({ type: 'dashboard', searchSource: true });
const response = {
found: true,
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: '\"{\\n \\\"filter\\\": []\\n}\"'
}
}
};
expect(savedObject.attributes).to.be(attributes);
try {
await savedObject.applyESResp(response);
throw new Error('applyESResp should have failed, but did not.');
} catch (err) {
expect(err instanceof InvalidJSONProperty).to.be(true);
}
});
it('preserves original defaults if not overridden', function () {
const id = 'anid';
const preserveMeValue = 'here to stay!';
const config = {
defaults: {
preserveMe: preserveMeValue
},
type: 'dashboard',
id: id
};
const mockDocResponse = getMockedDocResponse(id);
stubESResponse(mockDocResponse);
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
expect(savedObject._source.preserveMe).to.equal(preserveMeValue);
const response = { found: true, _source: {} };
return savedObject.applyESResp(response);
}).then(() => {
expect(savedObject._source.preserveMe).to.equal(preserveMeValue);
});
});
it('overrides defaults', function () {
const id = 'anid';
const config = {
defaults: {
flower: 'rose'
},
type: 'dashboard',
id: id
};
const mockDocResponse = getMockedDocResponse(id);
stubESResponse(mockDocResponse);
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
expect(savedObject._source.flower).to.equal('rose');
const response = {
found: true,
_source: {
flower: 'orchid'
}
};
return savedObject.applyESResp(response);
}).then(() => {
expect(savedObject._source.flower).to.equal('orchid');
});
});
it('overrides previous _source and default values', function () {
const id = 'anid';
const config = {
defaults: {
dinosaurs: {
tRex: 'is the scariest'
}
},
type: 'dashboard',
id: id
};
const mockDocResponse = getMockedDocResponse(
id,
{ attributes: { dinosaurs: { tRex: 'is not so bad' }, } });
stubESResponse(mockDocResponse);
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
const response = {
found: true,
_source: { dinosaurs: { tRex: 'has big teeth' } }
};
return savedObject.applyESResp(response);
})
.then(() => {
expect(savedObject._source.dinosaurs.tRex).to.equal('has big teeth');
});
});
it('does not inject references when references array is missing', async () => {
const injectReferences = sinon.stub();
const config = {
type: 'dashboard',
injectReferences,
};
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
const response = {
found: true,
_source: {
dinosaurs: { tRex: 'has big teeth' },
},
};
return savedObject.applyESResp(response);
})
.then(() => {
expect(injectReferences).to.have.property('notCalled', true);
});
});
it('does not inject references when references array is empty', async () => {
const injectReferences = sinon.stub();
const config = {
type: 'dashboard',
injectReferences,
};
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
const response = {
found: true,
_source: {
dinosaurs: { tRex: 'has big teeth' },
},
references: [],
};
return savedObject.applyESResp(response);
})
.then(() => {
expect(injectReferences).to.have.property('notCalled', true);
});
});
it('injects references when function is provided and references exist', async () => {
const injectReferences = sinon.stub();
const config = {
type: 'dashboard',
injectReferences,
};
const savedObject = new SavedObject(config);
return savedObject.init()
.then(() => {
const response = {
found: true,
_source: {
dinosaurs: { tRex: 'has big teeth' },
},
references: [{}],
};
return savedObject.applyESResp(response);
})
.then(() => {
expect(injectReferences).to.have.property('calledOnce', true);
});
});
it('injects references from searchSourceJSON', async () => {
const savedObject = new SavedObject({ type: 'dashboard', searchSource: true });
return savedObject
.init()
.then(() => {
const response = {
found: true,
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
filter: [
{
meta: {
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
},
},
],
}),
},
},
references: [
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
id: 'my-index-1',
},
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
type: 'index-pattern',
id: 'my-index-2',
},
],
};
savedObject.applyESResp(response);
expect(savedObject.searchSource.getFields()).to.eql({
index: 'my-index-1',
filter: [
{
meta: {
index: 'my-index-2',
},
},
],
});
});
});
});
it('persists version', () => {
const version = 2;
describe ('config', function () {
const client = sinon.stub();
const savedObject = new SavedObject(client, { version });
expect(savedObject._version).to.be(version);
it('afterESResp is called', function () {
const afterESRespCallback = sinon.spy();
const config = {
type: 'dashboard',
afterESResp: afterESRespCallback
};
return createInitializedSavedObject(config).then(() => {
expect(afterESRespCallback.called).to.be(true);
});
});
it('init is called', function () {
const initCallback = sinon.spy();
const config = {
type: 'dashboard',
init: initCallback
};
return createInitializedSavedObject(config).then(() => {
expect(initCallback.called).to.be(true);
});
});
describe('searchSource', function () {
it('when true, creates index', function () {
const indexPatternId = 'testIndexPattern';
const afterESRespCallback = sinon.spy();
const config = {
type: 'dashboard',
afterESResp: afterESRespCallback,
searchSource: true,
indexPattern: indexPatternId
};
stubESResponse({
id: indexPatternId,
type: 'dashboard',
attributes: {
title: 'testIndexPattern'
},
_version: 'foo'
});
const savedObject = new SavedObject(config);
expect(!!savedObject.searchSource.getField('index')).to.be(false);
return savedObject.init().then(() => {
expect(afterESRespCallback.called).to.be(true);
const index = savedObject.searchSource.getField('index');
expect(index instanceof IndexPattern).to.be(true);
expect(index.id).to.equal(indexPatternId);
});
});
it('when false, does not create index', function () {
const indexPatternId = 'testIndexPattern';
const afterESRespCallback = sinon.spy();
const config = {
type: 'dashboard',
afterESResp: afterESRespCallback,
searchSource: false,
indexPattern: indexPatternId
};
stubESResponse(getMockedDocResponse(indexPatternId));
const savedObject = new SavedObject(config);
expect(!!savedObject.searchSource).to.be(false);
return savedObject.init().then(() => {
expect(afterESRespCallback.called).to.be(true);
expect(!!savedObject.searchSource).to.be(false);
});
});
});
describe('type', function () {
it('that is not specified throws an error', function () {
const config = {};
const savedObject = new SavedObject(config);
try {
savedObject.init();
expect(false).to.be(true);
} catch (err) {
expect(err).to.not.be(null);
}
});
it('that is invalid invalid throws an error', function () {
const config = { type: 'notypeexists' };
const savedObject = new SavedObject(config);
try {
savedObject.init();
expect(false).to.be(true);
} catch (err) {
expect(err).to.not.be(null);
}
});
it('that is valid passes', function () {
const config = { type: 'dashboard' };
return new SavedObject(config).init();
});
});
describe('defaults', function () {
function getTestDefaultConfig(extraOptions) {
return {
defaults: { testDefault: 'hi' },
type: 'dashboard',
...extraOptions
};
}
function expectDefaultApplied(config) {
return createInitializedSavedObject(config).then((savedObject) => {
expect(savedObject.defaults).to.be(config.defaults);
});
}
describe('applied to object when id', function () {
it('is not specified', function () {
expectDefaultApplied(getTestDefaultConfig());
});
it('is undefined', function () {
expectDefaultApplied(getTestDefaultConfig({ id: undefined }));
});
it('is 0', function () {
expectDefaultApplied(getTestDefaultConfig({ id: 0 }));
});
it('is false', function () {
expectDefaultApplied(getTestDefaultConfig({ id: false }));
});
});
it('applied to source if an id is given', function () {
const myId = 'myid';
const customDefault = 'hi';
const initialOverwriteMeValue = 'this should get overwritten by the server response';
const config = {
defaults: {
overwriteMe: initialOverwriteMeValue,
customDefault: customDefault
},
type: 'dashboard',
id: myId
};
const serverValue = 'this should override the initial default value given';
const mockDocResponse = getMockedDocResponse(
myId,
{ attributes: { overwriteMe: serverValue } });
stubESResponse(mockDocResponse);
return createInitializedSavedObject(config).then((savedObject) => {
expect(!!savedObject._source).to.be(true);
expect(savedObject.defaults).to.be(config.defaults);
expect(savedObject._source.overwriteMe).to.be(serverValue);
expect(savedObject._source.customDefault).to.be(customDefault);
});
});
});
});
});

View file

@ -0,0 +1,52 @@
/*
* 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 sinon from 'sinon';
import expect from 'expect.js';
import { SimpleSavedObject } from '../simple_saved_object';
describe('SimpleSavedObject', () => {
it('persists type and id', () => {
const id = 'logstash-*';
const type = 'index-pattern';
const client = sinon.stub();
const savedObject = new SimpleSavedObject(client, { id, type });
expect(savedObject.id).to.be(id);
expect(savedObject.type).to.be(type);
});
it('persists attributes', () => {
const attributes = { title: 'My title' };
const client = sinon.stub();
const savedObject = new SimpleSavedObject(client, { attributes });
expect(savedObject.attributes).to.be(attributes);
});
it('persists version', () => {
const version = 2;
const client = sinon.stub();
const savedObject = new SimpleSavedObject(client, { version });
expect(savedObject._version).to.be(version);
});
});

View file

@ -36,13 +36,13 @@ import { injectI18n } from '@kbn/i18n/react';
import { SavedObjectAttributes } from '../../../../server/saved_objects';
import { VisTypesRegistryProvider } from '../../registry/vis_types';
import { SavedObject } from '../saved_object';
import { SimpleSavedObject } from '../simple_saved_object';
interface SavedObjectFinderUIState {
items: Array<{
title: string | null;
id: SavedObject<SavedObjectAttributes>['id'];
type: SavedObject<SavedObjectAttributes>['type'];
id: SimpleSavedObject<SavedObjectAttributes>['id'];
type: SimpleSavedObject<SavedObjectAttributes>['type'];
}>;
filter: string;
isFetchingItems: boolean;
@ -55,10 +55,10 @@ interface SavedObjectFinderUIState {
interface SavedObjectFinderUIProps extends InjectedIntlProps {
callToActionButton?: React.ReactNode;
onChoose?: (
id: SavedObject<SavedObjectAttributes>['id'],
type: SavedObject<SavedObjectAttributes>['type']
id: SimpleSavedObject<SavedObjectAttributes>['id'],
type: SimpleSavedObject<SavedObjectAttributes>['type']
) => void;
makeUrl?: (id: SavedObject<SavedObjectAttributes>['id']) => void;
makeUrl?: (id: SimpleSavedObject<SavedObjectAttributes>['id']) => void;
noItemsMessage?: React.ReactNode;
savedObjectType: 'visualization' | 'search';
visTypes?: VisTypesRegistryProvider;
@ -273,7 +273,7 @@ class SavedObjectFinderUI extends React.Component<
defaultMessage: 'Title',
}),
sortable: true,
render: (title: string, record: SavedObject<SavedObjectAttributes>) => {
render: (title: string, record: SimpleSavedObject<SavedObjectAttributes>) => {
const { onChoose, makeUrl } = this.props;
if (!onChoose && !makeUrl) {

View file

@ -19,8 +19,8 @@
import { find } from 'lodash';
import { SavedObjectAttributes } from '../../../server/saved_objects';
import { SavedObject } from './saved_object';
import { SavedObjectsClient } from './saved_objects_client';
import { SimpleSavedObject } from './simple_saved_object';
/**
* Returns an object matching a given title
@ -28,13 +28,13 @@ import { SavedObjectsClient } from './saved_objects_client';
* @param savedObjectsClient {SavedObjectsClient}
* @param type {string}
* @param title {string}
* @returns {Promise<SavedObject|undefined>}
* @returns {Promise<SimpleSavedObject|undefined>}
*/
export function findObjectByTitle<T extends SavedObjectAttributes>(
savedObjectsClient: SavedObjectsClient,
type: string,
title: string
): Promise<SavedObject<T> | void> {
): Promise<SimpleSavedObject<T> | void> {
if (!title) {
return Promise.resolve();
}

View file

@ -20,5 +20,7 @@
export { SavedObjectsClient } from './saved_objects_client';
export { SavedObjectRegistryProvider } from './saved_object_registry';
export { SavedObjectsClientProvider } from './saved_objects_client_provider';
export { SavedObject } from './saved_object';
// @ts-ignore
export { SavedObjectLoader } from './saved_object_loader';
export { SimpleSavedObject } from './simple_saved_object';
export { findObjectByTitle } from './find_object_by_title';

View file

@ -31,20 +31,21 @@
import angular from 'angular';
import _ from 'lodash';
import { InvalidJSONProperty, SavedObjectNotFound } from '../../errors';
import MappingSetupProvider from '../../utils/mapping_setup';
import { InvalidJSONProperty, SavedObjectNotFound } from '../errors';
import MappingSetupProvider from '../utils/mapping_setup';
import { SearchSourceProvider } from '../search_source';
import { SavedObjectsClientProvider, findObjectByTitle } from '../../saved_objects';
import { migrateLegacyQuery } from '../../utils/migrate_legacy_query';
import { recentlyAccessed } from '../../persisted_log';
import { SearchSourceProvider } from '../courier/search_source';
import { findObjectByTitle } from './find_object_by_title';
import { SavedObjectsClientProvider } from './saved_objects_client_provider';
import { migrateLegacyQuery } from '../utils/migrate_legacy_query';
import { recentlyAccessed } from '../persisted_log';
import { i18n } from '@kbn/i18n';
/**
* An error message to be used when the user rejects a confirm overwrite.
* @type {string}
*/
const OVERWRITE_REJECTED = i18n.translate('common.ui.courier.savedObject.overwriteRejectedDescription', {
const OVERWRITE_REJECTED = i18n.translate('common.ui.savedObjects.overwriteRejectedDescription', {
defaultMessage: 'Overwrite confirmation was rejected'
});
@ -52,7 +53,7 @@ const OVERWRITE_REJECTED = i18n.translate('common.ui.courier.savedObject.overwri
* An error message to be used when the user rejects a confirm save with duplicate title.
* @type {string}
*/
const SAVE_DUPLICATE_REJECTED = i18n.translate('common.ui.courier.savedObject.saveDuplicateRejectedDescription', {
const SAVE_DUPLICATE_REJECTED = i18n.translate('common.ui.savedObjects.saveDuplicateRejectedDescription', {
defaultMessage: 'Save with duplicate title confirmation was rejected'
});
@ -70,6 +71,15 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
const SearchSource = Private(SearchSourceProvider);
const mappingSetup = Private(MappingSetupProvider);
/**
* The SavedObject class is a base class for saved objects loaded from the server and
* provides additional functionality besides loading/saving/deleting/etc.
*
* It is overloaded and configured to provide type-aware functionality.
* To just retrieve the attributes of saved objects, it is recommended to use SavedObjectLoader
* which returns instances of SimpleSavedObject which don't introduce additional type-specific complexity.
* @param {*} config
*/
function SavedObject(config) {
if (!_.isObject(config)) config = {};
@ -388,16 +398,16 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
.catch(err => {
// record exists, confirm overwriting
if (_.get(err, 'res.status') === 409) {
const confirmMessage = i18n.translate('common.ui.courier.savedObject.confirmModal.overwriteConfirmationMessage', {
const confirmMessage = i18n.translate('common.ui.savedObjects.confirmModal.overwriteConfirmationMessage', {
defaultMessage: 'Are you sure you want to overwrite {title}?',
values: { title: this.title }
});
return confirmModalPromise(confirmMessage, {
confirmButtonText: i18n.translate('common.ui.courier.savedObject.confirmModal.overwriteButtonLabel', {
confirmButtonText: i18n.translate('common.ui.savedObjects.confirmModal.overwriteButtonLabel', {
defaultMessage: 'Overwrite',
}),
title: i18n.translate('common.ui.courier.savedObject.confirmModal.overwriteTitle', {
title: i18n.translate('common.ui.savedObjects.confirmModal.overwriteTitle', {
defaultMessage: 'Overwrite {name}?',
values: { name: this.getDisplayName() }
}),
@ -410,13 +420,13 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
};
const displayDuplicateTitleConfirmModal = () => {
const confirmMessage = i18n.translate('common.ui.courier.savedObject.confirmModal.saveDuplicateConfirmationMessage', {
const confirmMessage = i18n.translate('common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage', {
defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`,
values: { title: this.title, name: this.getDisplayName() }
});
return confirmModalPromise(confirmMessage, {
confirmButtonText: i18n.translate('common.ui.courier.savedObject.confirmModal.saveDuplicateButtonLabel', {
confirmButtonText: i18n.translate('common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel', {
defaultMessage: 'Save {name}',
values: { name: this.getDisplayName() }
})

View file

@ -17,23 +17,24 @@
* under the License.
*/
import { Scanner } from '../../utils/scanner';
import { StringUtils } from '../../utils/string_utils';
import { StringUtils } from '../utils/string_utils';
/**
* The SavedObjectLoader class provides some convenience functions
* to load and save one kind of saved objects (specified in the constructor).
*
* It is based on the SavedObjectClient which implements loading and saving
* in an abstract, type-agnostic way. If possible, use SavedObjectClient directly
* to avoid pulling in extra functionality which isn't used.
*/
export class SavedObjectLoader {
constructor(SavedObjectClass, kbnIndex, kbnUrl, $http, chrome, savedObjectClient) {
constructor(SavedObjectClass, kbnUrl, chrome, savedObjectClient) {
this.type = SavedObjectClass.type;
this.Class = SavedObjectClass;
this.lowercaseType = this.type.toLowerCase();
this.kbnIndex = kbnIndex;
this.kbnUrl = kbnUrl;
this.chrome = chrome;
this.scanner = new Scanner($http, {
index: kbnIndex,
type: this.lowercaseType
});
this.loaderProperties = {
name: `${ this.lowercaseType }s`,
noun: StringUtils.upperFirst(this.type),
@ -85,13 +86,6 @@ export class SavedObjectLoader {
return source;
}
scanAll(queryString, pageSize = 1000) {
return this.scanner.scanAndMap(queryString, {
pageSize,
docCount: Infinity
});
}
/**
* Updates hit.attributes to contain an id and url field, and returns the updated
* attributes object.

View file

@ -21,8 +21,8 @@ jest.mock('ui/kfetch', () => ({}));
import * as sinon from 'sinon';
import { FindOptions } from '../../../server/saved_objects/service';
import { SavedObject } from './saved_object';
import { SavedObjectsClient } from './saved_objects_client';
import { SimpleSavedObject } from './simple_saved_object';
describe('SavedObjectsClient', () => {
const doc = {
@ -78,7 +78,7 @@ describe('SavedObjectsClient', () => {
test('resolves with instantiated SavedObject', async () => {
const response = await savedObjectsClient.get(doc.type, doc.id);
expect(response).toBeInstanceOf(SavedObject);
expect(response).toBeInstanceOf(SimpleSavedObject);
expect(response.type).toBe('config');
expect(response.get('title')).toBe('Example title');
});
@ -292,7 +292,7 @@ describe('SavedObjectsClient', () => {
const response = await savedObjectsClient.bulkCreate([doc], {});
expect(response).toHaveProperty('savedObjects');
expect(response.savedObjects.length).toBe(1);
expect(response.savedObjects[0]).toBeInstanceOf(SavedObject);
expect(response.savedObjects[0]).toBeInstanceOf(SimpleSavedObject);
});
test('makes HTTP call', async () => {

View file

@ -31,7 +31,7 @@ import { CreateResponse, FindOptions, UpdateResponse } from '../../../server/sav
import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index';
import { kfetch, KFetchQuery } from '../kfetch';
import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../utils/case_conversion';
import { SavedObject } from './saved_object';
import { SimpleSavedObject } from './simple_saved_object';
interface RequestParams {
method: 'POST' | 'GET' | 'PUT' | 'DELETE';
@ -60,7 +60,7 @@ interface UpdateOptions {
}
interface BatchResponse<T extends SavedObjectAttributes = SavedObjectAttributes> {
savedObjects: Array<SavedObject<T>>;
savedObjects: Array<SimpleSavedObject<T>>;
}
interface FindResults<T extends SavedObjectAttributes = SavedObjectAttributes>
@ -73,7 +73,9 @@ interface FindResults<T extends SavedObjectAttributes = SavedObjectAttributes>
interface BatchQueueEntry {
type: string;
id: string;
resolve: <T extends SavedObjectAttributes>(value: SavedObject<T> | PlainSavedObject<T>) => void;
resolve: <T extends SavedObjectAttributes>(
value: SimpleSavedObject<T> | PlainSavedObject<T>
) => void;
reject: (reason?: any) => void;
}
@ -91,6 +93,14 @@ const BATCH_INTERVAL = 100;
const API_BASE_URL = '/api/saved_objects/';
/**
* The SavedObjectsClient class acts as a generic data fetcher
* and data saver for saved objects regardless of type.
*
* If possible, this class should be used to load saved objects
* instead of the SavedObjectLoader class which implements some
* additional functionality.
*/
export class SavedObjectsClient {
/**
* Throttled processing of get requests into bulk requests at 100ms interval
@ -145,7 +155,7 @@ export class SavedObjectsClient {
type: string,
attributes: T,
options: CreateOptions = {}
): Promise<SavedObject<T>> => {
): Promise<SimpleSavedObject<T>> => {
if (!type || !attributes) {
return Promise.reject(new Error('requires type and attributes'));
}
@ -257,7 +267,7 @@ export class SavedObjectsClient {
public get = <T extends SavedObjectAttributes>(
type: string,
id: string
): Promise<SavedObject<T>> => {
): Promise<SimpleSavedObject<T>> => {
if (!type || !id) {
return Promise.reject(new Error('requires type and id'));
}
@ -311,7 +321,7 @@ export class SavedObjectsClient {
id: string,
attributes: T,
{ version, migrationVersion, references }: UpdateOptions = {}
): Promise<SavedObject<T>> {
): Promise<SimpleSavedObject<T>> {
if (!type || !id || !attributes) {
return Promise.reject(new Error('requires type, id and attributes'));
}
@ -336,8 +346,8 @@ export class SavedObjectsClient {
private createSavedObject<T extends SavedObjectAttributes>(
options: PlainSavedObject<T>
): SavedObject<T> {
return new SavedObject(this, options);
): SimpleSavedObject<T> {
return new SimpleSavedObject(this, options);
}
private getPath(path: Array<string | undefined>): string {

View file

@ -24,7 +24,15 @@ import {
} from '../../../server/saved_objects';
import { SavedObjectsClient } from './saved_objects_client';
export class SavedObject<T extends SavedObjectAttributes> {
/**
* This class is a very simple wrapper for SavedObjects loaded from the server.
*
* It provides basic functionality for updating/deleting/etc. saved objects but
* doesn't include any type-specific implementations.
*
* For more sophisiticated use cases, the SavedObject class implements additional functions
*/
export class SimpleSavedObject<T extends SavedObjectAttributes> {
public attributes: T;
// tslint:disable-next-line variable-name We want to use the same interface this class had in JS
public _version?: SavedObjectType<T>['version'];

View file

@ -2,10 +2,10 @@
<div
ng-hide="!savedObject.isTitleChanged() || savedObject.copyOnSave"
class="kuiLocalDropdownWarning kuiVerticalRhythmSmall"
i18n-id="common.ui.courier.savedObject.howToSaveAsNewDescription"
i18n-id="common.ui.savedObjects.howToSaveAsNewDescription"
i18n-default-message="In previous versions of Kibana, changing the name of a {savedObjectName} would make a copy with the new name. Use the 'Save as a new {savedObjectName}' checkbox to do this now."
i18n-values="{ savedObjectName: savedObject.getDisplayName() }"
i18n-description="'Save as a new {savedObjectName}' refers to common.ui.courier.savedObject.saveAsNewLabel and should be the same text."
i18n-description="'Save as a new {savedObjectName}' refers to common.ui.savedObjects.saveAsNewLabel and should be the same text."
></div>
<label class="kuiCheckBoxLabel kuiVerticalRhythmSmall">
@ -19,7 +19,7 @@
<span
class="kuiCheckBoxLabel__text"
i18n-id="common.ui.courier.savedObject.saveAsNewLabel"
i18n-id="common.ui.savedObjects.saveAsNewLabel"
i18n-default-message="Save as a new {savedObjectName}"
i18n-values="{ savedObjectName: savedObject.getDisplayName() }"
></span>

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { uiModules } from '../../../modules';
import { uiModules } from '../../modules';
import saveObjectSaveAsCheckboxTemplate from './saved_object_save_as_checkbox.html';
uiModules

View file

@ -1,141 +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 { Scanner } from '../scanner';
import expect from 'expect.js';
import 'elasticsearch-browser';
import ngMock from 'ng_mock';
import sinon from 'sinon';
describe('Scanner', function () {
let http;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($http) {
http = $http;
}));
afterEach(function () {
http = null;
});
describe('initialization', function () {
it('should throw errors if missing arguments on initialization', function () {
expect(() => new Scanner()).to.throwError();
expect(() => new Scanner(http)).to.throwError();
expect(() => new Scanner(http, {
index: 'foo',
type: 'bar'
})).not.to.throwError();
});
});
describe('scan', function () {
let httpPost;
let search;
let scroll;
let scanner;
const mockSearch = { '_scroll_id': 'abc', 'took': 1, 'timed_out': false, '_shards': { 'total': 1, 'successful': 1, 'failed': 0 }, 'hits': { 'total': 2, 'max_score': 0.0, 'hits': [] } }; // eslint-disable-line max-len
const hits = [{
_id: 'one',
_type: 'config',
_source: { title: 'First title' }
}, {
_id: 'two',
_type: 'config',
_source: { title: 'Second title' }
}];
const mockScroll = { 'took': 1, 'timed_out': false, '_shards': { 'total': 1, 'successful': 1, 'failed': 0 }, 'hits': { 'total': 2, 'max_score': 0.0, 'hits': hits } }; // eslint-disable-line max-len
beforeEach(function () {
scanner = new Scanner(http, {
index: 'foo',
type: 'bar'
});
search = sinon.stub().returns(Promise.resolve({ data: mockSearch }));
scroll = sinon.stub().returns(Promise.resolve({ data: mockScroll }));
httpPost = sinon.stub(scanner.$http, 'post').callsFake((path, ...args) => {
if (path.includes('legacy_scroll_start')) {
return search(...args);
}
if (path.includes('legacy_scroll_continue')) {
return scroll(...args);
}
throw new Error(`Unexpected path to $http.post(): ${path}`);
});
});
it('should reject when an error occurs', function () {
search = search.returns(Promise.reject(new Error('fail.')));
return scanner.scanAndMap('')
.then(function () {
throw new Error('should reject');
})
.catch(function (error) {
expect(error.message).to.be('fail.');
});
});
it('should search and then scroll for results', function () {
return scanner.scanAndMap('')
.then(function () {
expect(search.called).to.be(true);
expect(scroll.called).to.be(true);
});
});
it('should map results if a function is provided', function () {
return scanner.scanAndMap(null, null, function (hit) {
return hit._id.toUpperCase();
})
.then(function (response) {
expect(response.hits[0]).to.be('ONE');
expect(response.hits[1]).to.be('TWO');
});
});
it('should only return the requested number of documents', function () {
return scanner.scanAndMap(null, { docCount: 1 }, function (hit) {
return hit._id.toUpperCase();
})
.then(function (response) {
expect(response.hits[0]).to.be('ONE');
expect(response.hits[1]).to.be(undefined);
});
});
it('should scroll across multiple pages', function () {
const oneResult = { 'took': 1, 'timed_out': false, '_shards': { 'total': 1, 'successful': 1, 'failed': 0 }, 'hits': { 'total': 2, 'max_score': 0.0, 'hits': ['one'] } }; // eslint-disable-line max-len
scroll = sinon.stub().returns(Promise.resolve({ data: oneResult }));
return scanner.scanAndMap(null, { pageSize: 1 })
.then(function (response) {
expect(scroll.calledTwice);
expect(response.hits.length).to.be(2);
expect(scroll.getCall(1).args[0].scrollId).to.be('abc');
expect(scroll.getCall(0).args[0].scrollId).to.be('abc');
});
});
afterEach(function () {
httpPost.restore();
});
});
});

View file

@ -1,142 +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 _ from 'lodash';
import chrome from '../chrome';
export const Scanner = function ($http, { index, type } = {}) {
if (!index) throw new Error('Expected index');
if (!type) throw new Error('Expected type');
if (!$http) throw new Error('Expected $http');
this.$http = $http;
this.index = index;
this.type = type;
};
Scanner.prototype.start = function (searchBody) {
const { addBasePath } = chrome;
const scrollStartPath = addBasePath('/api/kibana/legacy_scroll_start');
return this.$http.post(scrollStartPath, searchBody);
};
Scanner.prototype.continue = function (scrollId) {
const { addBasePath } = chrome;
const scrollContinuePath = addBasePath('/api/kibana/legacy_scroll_continue');
return this.$http.post(scrollContinuePath, { scrollId });
};
Scanner.prototype.scanAndMap = function (searchString, options, mapFn) {
const bool = { must: [], filter: [] };
let scrollId;
const allResults = {
hits: [],
total: 0
};
const opts = _.defaults(options || {}, {
pageSize: 100,
docCount: 1000
});
if (this.type) {
bool.filter.push({
bool: {
should: [
{
term: {
_type: this.type
}
},
{
term: {
type: this.type
}
}
]
}
});
}
if (searchString) {
bool.must.push({
simple_query_string: {
query: searchString + '*',
fields: ['title^3', 'description'],
default_operator: 'AND'
}
});
} else {
bool.must.push({
match_all: {}
});
}
return new Promise((resolve, reject) => {
const getMoreUntilDone = (error, response) => {
if (error) {
reject(error);
return;
}
const scanAllResults = opts.docCount === Infinity;
allResults.total = scanAllResults ? response.hits.total : Math.min(response.hits.total, opts.docCount);
scrollId = response._scroll_id || scrollId;
let hits = response.hits.hits
.slice(0, allResults.total - allResults.hits.length);
hits = hits.map(hit => {
if (hit._type === 'doc') {
return {
_id: hit._id.replace(`${this.type}:`, ''),
_type: this.type,
_source: hit._source[this.type],
_meta: {
savedObjectVersion: 2
}
};
}
return _.pick(hit, ['_id', '_type', '_source']);
});
if (mapFn) hits = hits.map(mapFn);
allResults.hits = allResults.hits.concat(hits);
const collectedAllResults = allResults.total === allResults.hits.length;
if (collectedAllResults) {
resolve(allResults);
} else {
this.continue(scrollId)
.then(response => getMoreUntilDone(null, response.data))
.catch(error => getMoreUntilDone(error));
}
};
const searchBody = {
index: this.index,
size: opts.pageSize,
body: { query: { bool } },
};
this.start(searchBody)
.then(response => getMoreUntilDone(null, response.data))
.catch(error => getMoreUntilDone(error));
});
};

View file

@ -5,7 +5,7 @@
*/
import { uiModules } from 'ui/modules';
import { SavedObjectProvider } from 'ui/courier';
import { SavedObjectProvider } from 'ui/saved_objects/saved_object';
import { i18n } from '@kbn/i18n';
import {
extractReferences,

View file

@ -6,9 +6,8 @@
import './saved_gis_map';
import { uiModules } from 'ui/modules';
import { SavedObjectLoader } from 'ui/courier/saved_object/saved_object_loader';
import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects';
import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
const module = uiModules.get('app/maps');
@ -22,5 +21,5 @@ SavedObjectRegistryProvider.register({
// This is the only thing that gets injected into controllers
module.service('gisMapSavedObjectLoader', function (Private, SavedGisMap, kbnIndex, kbnUrl, $http, chrome) {
const savedObjectClient = Private(SavedObjectsClientProvider);
return new SavedObjectLoader(SavedGisMap, kbnIndex, kbnUrl, $http, chrome, savedObjectClient);
return new SavedObjectLoader(SavedGisMap, kbnUrl, chrome, savedObjectClient);
});

View file

@ -7,7 +7,7 @@
import _ from 'lodash';
import { uiModules } from 'ui/modules';
import { createLegacyClass } from 'ui/utils/legacy_class';
import { SavedObjectProvider } from 'ui/courier';
import { SavedObjectProvider } from 'ui/saved_objects/saved_object';
import {
getTimeFilters,
getMapZoom,

View file

@ -299,15 +299,6 @@
"common.ui.courier.requestTimeDescription": "请求从浏览器到 Elasticsearch 以及返回的时间。不包括请求在队列中等候的时间。",
"common.ui.courier.requestTimeLabel": "请求时间",
"common.ui.courier.requestTimeValue": "{requestTime}ms",
"common.ui.courier.savedObject.confirmModal.overwriteButtonLabel": "覆盖",
"common.ui.courier.savedObject.confirmModal.overwriteConfirmationMessage": "确定要覆盖 “{title}”?",
"common.ui.courier.savedObject.confirmModal.overwriteTitle": "覆盖“{name}”?",
"common.ui.courier.savedObject.confirmModal.saveDuplicateButtonLabel": "保存“{name}”",
"common.ui.courier.savedObject.confirmModal.saveDuplicateConfirmationMessage": "具有标题 “{title}” 的 “{name}” 已存在。是否确定要保存?",
"common.ui.courier.savedObject.howToSaveAsNewDescription": "在 Kibana 的以前版本中,更改 {savedObjectName} 的名称将创建具有新名称的副本。使用“另存为新的 {savedObjectName}” 复选框可立即达到此目的。",
"common.ui.courier.savedObject.overwriteRejectedDescription": "已拒绝覆盖确认",
"common.ui.courier.savedObject.saveAsNewLabel": "另存为新的 {savedObjectName}",
"common.ui.courier.savedObject.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认",
"common.ui.directives.confirmClickButtonLabel": "是否确定?",
"common.ui.directives.fieldNameIcons.booleanAriaLabel": "布尔字段",
"common.ui.directives.fieldNameIcons.conflictFieldAriaLabel": "冲突字段",
@ -586,8 +577,17 @@
"common.ui.savedObjectFinder.sortByButtonLabeDescendingScreenReaderOnly": "降序",
"common.ui.savedObjectFinder.sortByButtonLabel": "名称",
"common.ui.savedObjectFinder.sortByButtonLabelScreenReaderOnly": "排序依据",
"common.ui.savedObjects.confirmModal.overwriteButtonLabel": "覆盖",
"common.ui.savedObjects.confirmModal.overwriteConfirmationMessage": "确定要覆盖 “{title}”?",
"common.ui.savedObjects.confirmModal.overwriteTitle": "覆盖“{name}”?",
"common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel": "保存“{name}”",
"common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage": "具有标题 “{title}” 的 “{name}” 已存在。是否确定要保存?",
"common.ui.savedObjects.finder.searchPlaceholder": "搜索……",
"common.ui.savedObjects.finder.titleLabel": "标题",
"common.ui.savedObjects.howToSaveAsNewDescription": "在 Kibana 的以前版本中,更改 {savedObjectName} 的名称将创建具有新名称的副本。使用“另存为新的 {savedObjectName}” 复选框可立即达到此目的。",
"common.ui.savedObjects.overwriteRejectedDescription": "已拒绝覆盖确认",
"common.ui.savedObjects.saveAsNewLabel": "另存为新的 {savedObjectName}",
"common.ui.savedObjects.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认",
"common.ui.savedObjects.saveModal.cancelButtonLabel": "取消",
"common.ui.savedObjects.saveModal.confirmSaveButtonLabel": "确认保存",
"common.ui.savedObjects.saveModal.duplicateTitleDescription": "单击 “{confirmSaveLabel}” 以保存标题重复的{objectType}",