mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* create saved object client that is native javascript * fix savedObjectClient unit tests * get saved object client from chrome when being used outside of angular * update error handlers to pull status code from FetchError * add some debug messages to failing functional test * revert changes to management/_objects * add clicks to import done in import objects test * take screenshots of test only failing in CI * remove functional test screenshot code since test is passing in CI now * remove unused file, clean up saved_objects_client test to not use global, add test to error_auto_create_index to ensure compatibility with kfetch errors * add body to kfetch error * update savedObjectClient.bulkCreate * add bulkCreate wrapper to SavedObjectsClientProvider * mark _createSavedObject and _processBatchQueue as private methods
This commit is contained in:
parent
e00ad0c08c
commit
988784188c
14 changed files with 376 additions and 235 deletions
|
@ -47,7 +47,6 @@ import * as filterActions from 'ui/doc_table/actions/filter';
|
|||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry';
|
||||
import { DashboardPanelActionsRegistryProvider } from 'ui/dashboard_panel_actions/dashboard_panel_actions_registry';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
|
||||
|
@ -86,7 +85,6 @@ app.directive('dashboardApp', function ($injector) {
|
|||
|
||||
panelActionsStore.initializeFromRegistry(panelActionsRegistry);
|
||||
|
||||
const savedObjectsClient = Private(SavedObjectsClientProvider);
|
||||
const visTypes = Private(VisTypesRegistryProvider);
|
||||
$scope.getEmbeddableFactory = panelType => embeddableFactories.byName[panelType];
|
||||
|
||||
|
@ -391,7 +389,7 @@ app.directive('dashboardApp', function ($injector) {
|
|||
const isLabsEnabled = config.get('visualize:enableLabs');
|
||||
const listingLimit = config.get('savedObjects:listingLimit');
|
||||
|
||||
showAddPanel(savedObjectsClient, dashboardStateManager.addNewPanel, addNewVis, listingLimit, isLabsEnabled, visTypes);
|
||||
showAddPanel(chrome.getSavedObjectsClient(), dashboardStateManager.addNewPanel, addNewVis, listingLimit, isLabsEnabled, visTypes);
|
||||
};
|
||||
updateViewMode(dashboardStateManager.getViewMode());
|
||||
|
||||
|
|
|
@ -18,14 +18,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
import { FetchFieldsProvider } from '../lib/fetch_fields';
|
||||
import { extractIndexPatterns } from '../lib/extract_index_patterns';
|
||||
|
||||
function ReactEditorControllerProvider(Private, config) {
|
||||
const fetchFields = Private(FetchFieldsProvider);
|
||||
const savedObjectsClient = Private(SavedObjectsClientProvider);
|
||||
const savedObjectsClient = chrome.getSavedObjectsClient();
|
||||
|
||||
class ReactEditorController {
|
||||
constructor(el, savedObj) {
|
||||
|
|
28
src/ui/public/chrome/api/saved_object_client.js
Normal file
28
src/ui/public/chrome/api/saved_object_client.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClient } from '../../saved_objects';
|
||||
|
||||
export function initSavedObjectClient(chrome) {
|
||||
const savedObjectClient = new SavedObjectsClient();
|
||||
|
||||
chrome.getSavedObjectsClient = function () {
|
||||
return savedObjectClient;
|
||||
};
|
||||
}
|
|
@ -44,6 +44,7 @@ import translationsApi from './api/translations';
|
|||
import { initChromeXsrfApi } from './api/xsrf';
|
||||
import { initUiSettingsApi } from './api/ui_settings';
|
||||
import { initLoadingCountApi } from './api/loading_count';
|
||||
import { initSavedObjectClient } from './api/saved_object_client';
|
||||
|
||||
export const chrome = {};
|
||||
const internals = _.defaults(
|
||||
|
@ -62,6 +63,7 @@ const internals = _.defaults(
|
|||
);
|
||||
|
||||
initUiSettingsApi(chrome);
|
||||
initSavedObjectClient(chrome);
|
||||
appsApi(chrome, internals);
|
||||
initChromeXsrfApi(chrome, internals);
|
||||
initChromeNavApi(chrome, internals);
|
||||
|
|
|
@ -81,6 +81,10 @@ describe('Saved Object', function () {
|
|||
return savedObject.init();
|
||||
}
|
||||
|
||||
const mock409FetchError = {
|
||||
res: { status: 409 }
|
||||
};
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
StubIndexPatternsApiClientModule,
|
||||
|
@ -104,7 +108,7 @@ describe('Saved Object', function () {
|
|||
describe('with confirmOverwrite', function () {
|
||||
function stubConfirmOverwrite() {
|
||||
window.confirm = sinon.stub().returns(true);
|
||||
sinon.stub(esDataStub, 'create').returns(BluebirdPromise.reject({ status: 409 }));
|
||||
sinon.stub(esDataStub, 'create').returns(BluebirdPromise.reject(mock409FetchError));
|
||||
}
|
||||
|
||||
describe('when true', function () {
|
||||
|
@ -112,7 +116,7 @@ describe('Saved Object', function () {
|
|||
stubESResponse(getMockedDocResponse('myId'));
|
||||
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
|
||||
const createStub = sinon.stub(savedObjectsClientStub, 'create');
|
||||
createStub.onFirstCall().returns(BluebirdPromise.reject({ statusCode: 409 }));
|
||||
createStub.onFirstCall().returns(BluebirdPromise.reject(mock409FetchError));
|
||||
createStub.onSecondCall().returns(BluebirdPromise.resolve({ id: 'myId' }));
|
||||
|
||||
stubConfirmOverwrite();
|
||||
|
@ -135,7 +139,7 @@ describe('Saved Object', function () {
|
|||
return createInitializedSavedObject({ type: 'dashboard', id: 'HI' }).then(savedObject => {
|
||||
window.confirm = sinon.stub().returns(false);
|
||||
|
||||
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.reject({ statusCode: 409 }));
|
||||
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.reject(mock409FetchError));
|
||||
|
||||
savedObject.lastSavedTitle = 'original title';
|
||||
savedObject.title = 'new title';
|
||||
|
@ -154,7 +158,7 @@ describe('Saved Object', function () {
|
|||
return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => {
|
||||
stubConfirmOverwrite();
|
||||
|
||||
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.reject({ statusCode: 409 }));
|
||||
sinon.stub(savedObjectsClientStub, 'create').returns(BluebirdPromise.reject(mock409FetchError));
|
||||
|
||||
return savedObject.save({ confirmOverwrite: true })
|
||||
.then(() => {
|
||||
|
|
|
@ -292,7 +292,7 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
|
|||
return savedObjectsClient.create(esType, source, { id: this.id })
|
||||
.catch(err => {
|
||||
// record exists, confirm overwriting
|
||||
if (_.get(err, 'statusCode') === 409) {
|
||||
if (_.get(err, 'res.status') === 409) {
|
||||
const confirmMessage = `Are you sure you want to overwrite ${this.title}?`;
|
||||
|
||||
return confirmModalPromise(confirmMessage, { confirmButtonText: `Overwrite ${this.getDisplayName()}` })
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
import { get } from 'lodash';
|
||||
|
||||
import uiRoutes from '../routes';
|
||||
import { KbnUrlProvider } from '../url';
|
||||
|
||||
import './error_auto_create_index.less';
|
||||
import template from './error_auto_create_index.html';
|
||||
|
@ -28,20 +27,13 @@ import template from './error_auto_create_index.html';
|
|||
uiRoutes
|
||||
.when('/error/action.auto_create_index', { template });
|
||||
|
||||
export function ErrorAutoCreateIndexProvider(Private, Promise) {
|
||||
const kbnUrl = Private(KbnUrlProvider);
|
||||
|
||||
return new (class ErrorAutoCreateIndex {
|
||||
test(error) {
|
||||
return (
|
||||
error.statusCode === 503 &&
|
||||
get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
takeover() {
|
||||
kbnUrl.change('/error/action.auto_create_index');
|
||||
return Promise.halt();
|
||||
}
|
||||
});
|
||||
export function isAutoCreateIndexError(error) {
|
||||
return (
|
||||
get(error, 'res.status') === 503 &&
|
||||
get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR'
|
||||
);
|
||||
}
|
||||
|
||||
export function showAutoCreateIndexErrorPage() {
|
||||
window.location.hash = '/error/action.auto_create_index';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('../chrome', () => ({
|
||||
addBasePath: path => `myBase/${path}`,
|
||||
}));
|
||||
jest.mock('../metadata', () => ({
|
||||
metadata: {
|
||||
version: 'my-version',
|
||||
},
|
||||
}));
|
||||
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import { isAutoCreateIndexError } from './error_auto_create_index';
|
||||
|
||||
describe('isAutoCreateIndexError correctly handles FetchError thrown by kfetch', () => {
|
||||
describe('404', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.post({
|
||||
matcher: '*',
|
||||
response: {
|
||||
status: 404,
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
test('should return false', async () => {
|
||||
let gotError = false;
|
||||
try {
|
||||
await kfetch({ method: 'POST', pathname: 'my/path' });
|
||||
} catch (fetchError) {
|
||||
gotError = true;
|
||||
expect(isAutoCreateIndexError(fetchError)).toBe(false);
|
||||
}
|
||||
|
||||
expect(gotError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('503 error that is not ES_AUTO_CREATE_INDEX_ERROR', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.post({
|
||||
matcher: '*',
|
||||
response: {
|
||||
status: 503,
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
test('should return false', async () => {
|
||||
let gotError = false;
|
||||
try {
|
||||
await kfetch({ method: 'POST', pathname: 'my/path' });
|
||||
} catch (fetchError) {
|
||||
gotError = true;
|
||||
expect(isAutoCreateIndexError(fetchError)).toBe(false);
|
||||
}
|
||||
|
||||
expect(gotError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('503 error that is ES_AUTO_CREATE_INDEX_ERROR', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.post({
|
||||
matcher: '*',
|
||||
response: {
|
||||
body: {
|
||||
code: 'ES_AUTO_CREATE_INDEX_ERROR'
|
||||
},
|
||||
status: 503,
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
test('should return true', async () => {
|
||||
let gotError = false;
|
||||
try {
|
||||
await kfetch({ method: 'POST', pathname: 'my/path' });
|
||||
} catch (fetchError) {
|
||||
gotError = true;
|
||||
expect(isAutoCreateIndexError(fetchError)).toBe(true);
|
||||
}
|
||||
|
||||
expect(gotError).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { ErrorAutoCreateIndexProvider } from './error_auto_create_index';
|
||||
export { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from './error_auto_create_index';
|
||||
|
|
|
@ -24,9 +24,10 @@ import { metadata } from '../metadata';
|
|||
import { merge } from 'lodash';
|
||||
|
||||
class FetchError extends Error {
|
||||
constructor(res) {
|
||||
constructor(res, body) {
|
||||
super(res.statusText);
|
||||
this.res = res;
|
||||
this.body = body;
|
||||
Error.captureStackTrace(this, FetchError);
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +60,13 @@ export async function kfetch(fetchOptions, kibanaOptions) {
|
|||
const res = await fetch(fullUrl, combinedFetchOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError(res);
|
||||
let body;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch (err) {
|
||||
// ignore error, may not be able to get body for response that is not ok
|
||||
}
|
||||
throw new FetchError(res, body);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
jest.mock('ui/kfetch', () => ({}));
|
||||
|
||||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
import { SavedObjectsClient } from '../saved_objects_client';
|
||||
import { SavedObject } from '../saved_object';
|
||||
|
||||
describe('SavedObjectsClient', () => {
|
||||
const basePath = Math.random().toString(36).substring(7);
|
||||
const sandbox = sinon.createSandbox();
|
||||
const doc = {
|
||||
id: 'AVwSwFxtcMV38qjDZoQg',
|
||||
type: 'config',
|
||||
|
@ -32,106 +32,73 @@ describe('SavedObjectsClient', () => {
|
|||
version: 2
|
||||
};
|
||||
|
||||
let kfetchStub;
|
||||
let savedObjectsClient;
|
||||
let $http;
|
||||
|
||||
beforeEach(() => {
|
||||
$http = sandbox.stub();
|
||||
savedObjectsClient = new SavedObjectsClient({
|
||||
$http,
|
||||
basePath
|
||||
});
|
||||
kfetchStub = sinon.stub();
|
||||
require('ui/kfetch').kfetch = async (...args) => {
|
||||
return kfetchStub(...args);
|
||||
};
|
||||
savedObjectsClient = new SavedObjectsClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
describe('#_getPath', () => {
|
||||
test('returns without arguments', () => {
|
||||
const path = savedObjectsClient._getPath();
|
||||
const expected = `/api/saved_objects/`;
|
||||
|
||||
describe('#_getUrl', () => {
|
||||
it('returns without arguments', () => {
|
||||
const url = savedObjectsClient._getUrl();
|
||||
const expected = `${basePath}/api/saved_objects/`;
|
||||
|
||||
expect(url).to.be(expected);
|
||||
expect(path).to.be(expected);
|
||||
});
|
||||
|
||||
it('appends path', () => {
|
||||
const url = savedObjectsClient._getUrl(['some', 'path']);
|
||||
const expected = `${basePath}/api/saved_objects/some/path`;
|
||||
test('appends path', () => {
|
||||
const path = savedObjectsClient._getPath(['some', 'path']);
|
||||
const expected = `/api/saved_objects/some/path`;
|
||||
|
||||
expect(url).to.be(expected);
|
||||
});
|
||||
|
||||
it('appends query', () => {
|
||||
const url = savedObjectsClient._getUrl(['some', 'path'], { foo: 'Foo', bar: 'Bar' });
|
||||
const expected = `${basePath}/api/saved_objects/some/path?foo=Foo&bar=Bar`;
|
||||
|
||||
expect(url).to.be(expected);
|
||||
expect(path).to.be(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_request', () => {
|
||||
const params = { foo: 'Foo', bar: 'Bar' };
|
||||
const body = { foo: 'Foo', bar: 'Bar' };
|
||||
|
||||
it('passes options to $http', () => {
|
||||
$http.withArgs({
|
||||
test('passes options to kfetch', () => {
|
||||
kfetchStub.withArgs({
|
||||
method: 'POST',
|
||||
url: '/api/path',
|
||||
data: params
|
||||
}).returns(Promise.resolve({ data: '' }));
|
||||
pathname: '/api/path',
|
||||
query: undefined,
|
||||
body: JSON.stringify(body)
|
||||
}).returns(Promise.resolve({}));
|
||||
|
||||
savedObjectsClient._request('POST', '/api/path', params);
|
||||
savedObjectsClient._request({ method: 'POST', path: '/api/path', body });
|
||||
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
});
|
||||
|
||||
it('throws error when body is provided for GET', async () => {
|
||||
test('throws error when body is provided for GET', async () => {
|
||||
try {
|
||||
await savedObjectsClient._request('GET', '/api/path', params);
|
||||
await savedObjectsClient._request({ method: 'GET', path: '/api/path', body });
|
||||
expect().fail('should have error');
|
||||
} catch (e) {
|
||||
expect(e.message).to.eql('body not permitted for GET requests');
|
||||
}
|
||||
});
|
||||
|
||||
it('catches API error', async () => {
|
||||
const message = 'Request failed';
|
||||
$http.returns(Promise.reject({ data: { error: message } }));
|
||||
|
||||
try {
|
||||
await savedObjectsClient._request('POST', '/api/path', params);
|
||||
expect().fail('should have error');
|
||||
} catch (e) {
|
||||
expect(e.message).to.eql(message);
|
||||
}
|
||||
});
|
||||
|
||||
it('catches API error status', async () => {
|
||||
$http.returns(Promise.reject({ status: 404 }));
|
||||
|
||||
try {
|
||||
await savedObjectsClient._request('POST', '/api/path', params);
|
||||
expect().fail('should have error');
|
||||
} catch (e) {
|
||||
expect(e.message).to.eql('404 Response');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get', () => {
|
||||
beforeEach(() => {
|
||||
$http.withArgs({
|
||||
kfetchStub.withArgs({
|
||||
method: 'POST',
|
||||
url: `${basePath}/api/saved_objects/_bulk_get`,
|
||||
data: sinon.match.any
|
||||
}).returns(Promise.resolve({ data: { saved_objects: [doc] } }));
|
||||
pathname: `/api/saved_objects/_bulk_get`,
|
||||
query: undefined,
|
||||
body: sinon.match.any
|
||||
}).returns(Promise.resolve({ saved_objects: [doc] }));
|
||||
});
|
||||
|
||||
it('returns a promise', () => {
|
||||
test('returns a promise', () => {
|
||||
expect(savedObjectsClient.get('index-pattern', 'logstash-*')).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('requires type', async () => {
|
||||
test('requires type', async () => {
|
||||
try {
|
||||
await savedObjectsClient.get();
|
||||
expect().fail('should have error');
|
||||
|
@ -140,7 +107,7 @@ describe('SavedObjectsClient', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('requires id', async () => {
|
||||
test('requires id', async () => {
|
||||
try {
|
||||
await savedObjectsClient.get('index-pattern');
|
||||
expect().throw('should have error');
|
||||
|
@ -149,7 +116,7 @@ describe('SavedObjectsClient', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('resolves with instantiated SavedObject', async () => {
|
||||
test('resolves with instantiated SavedObject', async () => {
|
||||
const response = await savedObjectsClient.get(doc.type, doc.id);
|
||||
expect(response).to.be.a(SavedObject);
|
||||
expect(response.type).to.eql('config');
|
||||
|
@ -157,26 +124,27 @@ describe('SavedObjectsClient', () => {
|
|||
expect(response._client).to.be.a(SavedObjectsClient);
|
||||
});
|
||||
|
||||
it('makes HTTP call', async () => {
|
||||
test('makes HTTP call', async () => {
|
||||
await savedObjectsClient.get(doc.type, doc.id);
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
beforeEach(() => {
|
||||
$http.withArgs({
|
||||
kfetchStub.withArgs({
|
||||
method: 'DELETE',
|
||||
url: `${basePath}/api/saved_objects/index-pattern/logstash-*`,
|
||||
data: undefined
|
||||
}).returns(Promise.resolve({ data: 'api-response' }));
|
||||
pathname: `/api/saved_objects/index-pattern/logstash-*`,
|
||||
query: undefined,
|
||||
body: undefined,
|
||||
}).returns(Promise.resolve({}));
|
||||
});
|
||||
|
||||
it('returns a promise', () => {
|
||||
test('returns a promise', () => {
|
||||
expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('requires type', async () => {
|
||||
test('requires type', async () => {
|
||||
try {
|
||||
await savedObjectsClient.delete();
|
||||
expect().throw('should have error');
|
||||
|
@ -185,7 +153,7 @@ describe('SavedObjectsClient', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('requires id', async () => {
|
||||
test('requires id', async () => {
|
||||
try {
|
||||
await savedObjectsClient.delete('index-pattern');
|
||||
expect().throw('should have error');
|
||||
|
@ -194,9 +162,9 @@ describe('SavedObjectsClient', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('makes HTTP call', () => {
|
||||
test('makes HTTP call', () => {
|
||||
savedObjectsClient.delete('index-pattern', 'logstash-*');
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -204,18 +172,19 @@ describe('SavedObjectsClient', () => {
|
|||
const requireMessage = 'requires type, id and attributes';
|
||||
|
||||
beforeEach(() => {
|
||||
$http.withArgs({
|
||||
kfetchStub.withArgs({
|
||||
method: 'PUT',
|
||||
url: `${basePath}/api/saved_objects/index-pattern/logstash-*`,
|
||||
data: sinon.match.any
|
||||
pathname: `/api/saved_objects/index-pattern/logstash-*`,
|
||||
query: undefined,
|
||||
body: sinon.match.any
|
||||
}).returns(Promise.resolve({ data: 'api-response' }));
|
||||
});
|
||||
|
||||
it('returns a promise', () => {
|
||||
test('returns a promise', () => {
|
||||
expect(savedObjectsClient.update('index-pattern', 'logstash-*', {})).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('requires type', async () => {
|
||||
test('requires type', async () => {
|
||||
try {
|
||||
await savedObjectsClient.update();
|
||||
expect().throw('should have error');
|
||||
|
@ -224,7 +193,7 @@ describe('SavedObjectsClient', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('requires id', async () => {
|
||||
test('requires id', async () => {
|
||||
try {
|
||||
await savedObjectsClient.update('index-pattern');
|
||||
expect().throw('should have error');
|
||||
|
@ -233,7 +202,7 @@ describe('SavedObjectsClient', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('requires attributes', async () => {
|
||||
test('requires attributes', async () => {
|
||||
try {
|
||||
await savedObjectsClient.update('index-pattern', 'logstash-*');
|
||||
expect().throw('should have error');
|
||||
|
@ -242,15 +211,15 @@ describe('SavedObjectsClient', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('makes HTTP call', () => {
|
||||
test('makes HTTP call', () => {
|
||||
const attributes = { foo: 'Foo', bar: 'Bar' };
|
||||
const body = { attributes, version: 2 };
|
||||
const options = { version: 2 };
|
||||
|
||||
savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options);
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
data: body
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
|
||||
body: JSON.stringify(body)
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@ -259,18 +228,19 @@ describe('SavedObjectsClient', () => {
|
|||
const requireMessage = 'requires type and attributes';
|
||||
|
||||
beforeEach(() => {
|
||||
$http.withArgs({
|
||||
kfetchStub.withArgs({
|
||||
method: 'POST',
|
||||
url: `${basePath}/api/saved_objects/index-pattern`,
|
||||
data: sinon.match.any
|
||||
}).returns(Promise.resolve({ data: 'api-response' }));
|
||||
pathname: `/api/saved_objects/index-pattern`,
|
||||
query: undefined,
|
||||
body: sinon.match.any
|
||||
}).returns(Promise.resolve({}));
|
||||
});
|
||||
|
||||
it('returns a promise', () => {
|
||||
test('returns a promise', () => {
|
||||
expect(savedObjectsClient.create('index-pattern', {})).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('requires type', async () => {
|
||||
test('requires type', async () => {
|
||||
try {
|
||||
await savedObjectsClient.create();
|
||||
expect().throw('should have error');
|
||||
|
@ -279,75 +249,104 @@ describe('SavedObjectsClient', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('allows for id to be provided', () => {
|
||||
test('allows for id to be provided', () => {
|
||||
const attributes = { foo: 'Foo', bar: 'Bar' };
|
||||
const url = `${basePath}/api/saved_objects/index-pattern/myId`;
|
||||
$http.withArgs({
|
||||
const path = `/api/saved_objects/index-pattern/myId`;
|
||||
kfetchStub.withArgs({
|
||||
method: 'POST',
|
||||
url,
|
||||
data: sinon.match.any
|
||||
}).returns(Promise.resolve({ data: 'api-response' }));
|
||||
pathname: path,
|
||||
query: undefined,
|
||||
body: sinon.match.any
|
||||
}).returns(Promise.resolve({}));
|
||||
|
||||
savedObjectsClient.create('index-pattern', attributes, { id: 'myId' });
|
||||
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
url
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
|
||||
pathname: path
|
||||
}));
|
||||
});
|
||||
|
||||
it('makes HTTP call', () => {
|
||||
test('makes HTTP call', () => {
|
||||
const attributes = { foo: 'Foo', bar: 'Bar' };
|
||||
savedObjectsClient.create('index-pattern', attributes);
|
||||
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
url: sinon.match.string,
|
||||
data: {
|
||||
attributes
|
||||
}
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
|
||||
pathname: sinon.match.string,
|
||||
body: JSON.stringify({ attributes }),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bulk_create', () => {
|
||||
beforeEach(() => {
|
||||
kfetchStub.withArgs({
|
||||
method: 'POST',
|
||||
pathname: `/api/saved_objects/_bulk_create`,
|
||||
query: sinon.match.any,
|
||||
body: sinon.match.any
|
||||
}).returns(Promise.resolve({ saved_objects: [doc] }));
|
||||
});
|
||||
|
||||
test('returns a promise', () => {
|
||||
expect(savedObjectsClient.bulkCreate([doc], {})).to.be.a(Promise);
|
||||
});
|
||||
|
||||
test('resolves with instantiated SavedObjects', async () => {
|
||||
const response = await savedObjectsClient.bulkCreate([doc], {});
|
||||
expect(response).to.have.property('savedObjects');
|
||||
expect(response.savedObjects.length).to.eql(1);
|
||||
expect(response.savedObjects[0]).to.be.a(SavedObject);
|
||||
});
|
||||
|
||||
test('makes HTTP call', async () => {
|
||||
await savedObjectsClient.bulkCreate([doc], {});
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#find', () => {
|
||||
const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' };
|
||||
|
||||
beforeEach(() => {
|
||||
$http.returns(Promise.resolve({ data: { saved_objects: [object] } }));
|
||||
kfetchStub.returns(Promise.resolve({ saved_objects: [object] }));
|
||||
});
|
||||
|
||||
it('returns a promise', () => {
|
||||
test('returns a promise', () => {
|
||||
expect(savedObjectsClient.find()).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('accepts type', () => {
|
||||
test('accepts type', () => {
|
||||
const body = { type: 'index-pattern', invalid: true };
|
||||
|
||||
savedObjectsClient.find(body);
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
url: `${basePath}/api/saved_objects/_find?type=index-pattern&invalid=true`
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
|
||||
pathname: `/api/saved_objects/_find`,
|
||||
query: { type: 'index-pattern', invalid: true }
|
||||
}));
|
||||
});
|
||||
|
||||
it('accepts fields', () => {
|
||||
test('accepts fields', () => {
|
||||
const body = { fields: ['title', 'description'] };
|
||||
|
||||
savedObjectsClient.find(body);
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.calledWithExactly($http, sinon.match({
|
||||
url: `${basePath}/api/saved_objects/_find?fields=title&fields=description`
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
sinon.assert.calledWithExactly(kfetchStub, sinon.match({
|
||||
pathname: `/api/saved_objects/_find`,
|
||||
query: { fields: [ 'title', 'description' ] }
|
||||
}));
|
||||
});
|
||||
|
||||
it('accepts from/size', () => {
|
||||
test('accepts from/size', () => {
|
||||
const body = { from: 50, size: 10 };
|
||||
|
||||
savedObjectsClient.find(body);
|
||||
sinon.assert.calledOnce($http);
|
||||
sinon.assert.alwaysCalledWith($http, sinon.match({
|
||||
url: `${basePath}/api/saved_objects/_find?from=50&size=10`
|
||||
sinon.assert.calledOnce(kfetchStub);
|
||||
sinon.assert.alwaysCalledWith(kfetchStub, sinon.match({
|
||||
pathname: `/api/saved_objects/_find`,
|
||||
query: { from: 50, size: 10 }
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -18,11 +18,12 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import chrome from '../chrome';
|
||||
|
||||
import { resolve as resolveUrl, format as formatUrl } from 'url';
|
||||
import { resolve as resolveUrl } from 'url';
|
||||
import { keysToSnakeCaseShallow, keysToCamelCaseShallow } from '../../../utils/case_conversion';
|
||||
import { SavedObject } from './saved_object';
|
||||
import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
|
||||
const join = (...uriComponents) => (
|
||||
uriComponents.filter(Boolean).map(encodeURIComponent).join('/')
|
||||
|
@ -34,19 +35,10 @@ const join = (...uriComponents) => (
|
|||
*/
|
||||
const BATCH_INTERVAL = 100;
|
||||
|
||||
export class SavedObjectsClient {
|
||||
constructor(options) {
|
||||
const {
|
||||
$http,
|
||||
basePath = chrome.getBasePath(),
|
||||
PromiseConstructor = Promise,
|
||||
onCreateFailure = () => {},
|
||||
} = options;
|
||||
const API_BASE_URL = '/api/saved_objects/';
|
||||
|
||||
this._$http = $http;
|
||||
this._apiBaseUrl = `${basePath}/api/saved_objects/`;
|
||||
this._PromiseCtor = PromiseConstructor;
|
||||
this._onCreateFailure = onCreateFailure;
|
||||
export class SavedObjectsClient {
|
||||
constructor() {
|
||||
this.batchQueue = [];
|
||||
}
|
||||
|
||||
|
@ -62,14 +54,21 @@ export class SavedObjectsClient {
|
|||
*/
|
||||
create(type, attributes = {}, options = {}) {
|
||||
if (!type || !attributes) {
|
||||
return this._PromiseCtor.reject(new Error('requires type and attributes'));
|
||||
return Promise.reject(new Error('requires type and attributes'));
|
||||
}
|
||||
|
||||
const url = this._getUrl([type, options.id], _.pick(options, ['overwrite']));
|
||||
const path = this._getPath([type, options.id]);
|
||||
const query = _.pick(options, ['overwrite']);
|
||||
|
||||
return this._request('POST', url, { attributes })
|
||||
.catch(this._onCreateFailure)
|
||||
.then(resp => this.createSavedObject(resp));
|
||||
return this._request({ method: 'POST', path, query, body: { attributes } })
|
||||
.catch(error => {
|
||||
if (isAutoCreateIndexError(error)) {
|
||||
return showAutoCreateIndexErrorPage();
|
||||
}
|
||||
|
||||
throw error;
|
||||
})
|
||||
.then(resp => this._createSavedObject(resp));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,10 +80,11 @@ export class SavedObjectsClient {
|
|||
* @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]}
|
||||
*/
|
||||
bulkCreate = (objects = [], options = {}) => {
|
||||
const url = this._getUrl(['_bulk_create'], _.pick(options, ['overwrite']));
|
||||
const path = this._getPath(['_bulk_create']);
|
||||
const query = _.pick(options, ['overwrite']);
|
||||
|
||||
return this._request('POST', url, objects).then(resp => {
|
||||
resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d));
|
||||
return this._request({ method: 'POST', path, query, body: objects }).then(resp => {
|
||||
resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d));
|
||||
return keysToCamelCaseShallow(resp);
|
||||
});
|
||||
}
|
||||
|
@ -98,10 +98,10 @@ export class SavedObjectsClient {
|
|||
*/
|
||||
delete(type, id) {
|
||||
if (!type || !id) {
|
||||
return this._PromiseCtor.reject(new Error('requires type and id'));
|
||||
return Promise.reject(new Error('requires type and id'));
|
||||
}
|
||||
|
||||
return this._request('DELETE', this._getUrl([type, id]));
|
||||
return this._request({ method: 'DELETE', path: this._getPath([type, id]) });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -118,10 +118,11 @@ export class SavedObjectsClient {
|
|||
* @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ]}
|
||||
*/
|
||||
find(options = {}) {
|
||||
const url = this._getUrl(['_find'], keysToSnakeCaseShallow(options));
|
||||
const path = this._getPath(['_find']);
|
||||
const query = keysToSnakeCaseShallow(options);
|
||||
|
||||
return this._request('GET', url).then(resp => {
|
||||
resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d));
|
||||
return this._request({ method: 'GET', path, query }).then(resp => {
|
||||
resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d));
|
||||
return keysToCamelCaseShallow(resp);
|
||||
});
|
||||
}
|
||||
|
@ -135,12 +136,12 @@ export class SavedObjectsClient {
|
|||
*/
|
||||
get(type, id) {
|
||||
if (!type || !id) {
|
||||
return this._PromiseCtor.reject(new Error('requires type and id'));
|
||||
return Promise.reject(new Error('requires type and id'));
|
||||
}
|
||||
|
||||
return new this._PromiseCtor((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.batchQueue.push({ type, id, resolve, reject });
|
||||
this.processBatchQueue();
|
||||
this._processBatchQueue();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -157,11 +158,11 @@ export class SavedObjectsClient {
|
|||
* ])
|
||||
*/
|
||||
bulkGet(objects = []) {
|
||||
const url = this._getUrl(['_bulk_get']);
|
||||
const path = this._getPath(['_bulk_get']);
|
||||
const filteredObjects = objects.map(obj => _.pick(obj, ['id', 'type']));
|
||||
|
||||
return this._request('POST', url, filteredObjects).then(resp => {
|
||||
resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d));
|
||||
return this._request({ method: 'POST', path, body: filteredObjects }).then(resp => {
|
||||
resp.saved_objects = resp.saved_objects.map(d => this._createSavedObject(d));
|
||||
return keysToCamelCaseShallow(resp);
|
||||
});
|
||||
}
|
||||
|
@ -177,23 +178,24 @@ export class SavedObjectsClient {
|
|||
*/
|
||||
update(type, id, attributes, { version } = {}) {
|
||||
if (!type || !id || !attributes) {
|
||||
return this._PromiseCtor.reject(new Error('requires type, id and attributes'));
|
||||
return Promise.reject(new Error('requires type, id and attributes'));
|
||||
}
|
||||
|
||||
const path = this._getPath([type, id]);
|
||||
const body = {
|
||||
attributes,
|
||||
version
|
||||
};
|
||||
|
||||
return this._request('PUT', this._getUrl([type, id]), body).then(resp => {
|
||||
return this.createSavedObject(resp);
|
||||
return this._request({ method: 'PUT', path, body }).then(resp => {
|
||||
return this._createSavedObject(resp);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttled processing of get requests into bulk requests at 100ms interval
|
||||
*/
|
||||
processBatchQueue = _.throttle(() => {
|
||||
_processBatchQueue = _.throttle(() => {
|
||||
const queue = _.cloneDeep(this.batchQueue);
|
||||
this.batchQueue = [];
|
||||
|
||||
|
@ -204,7 +206,7 @@ export class SavedObjectsClient {
|
|||
});
|
||||
|
||||
if (!foundObject) {
|
||||
return queueItem.resolve(this.createSavedObject(_.pick(queueItem, ['id', 'type'])));
|
||||
return queueItem.resolve(this._createSavedObject(_.pick(queueItem, ['id', 'type'])));
|
||||
}
|
||||
|
||||
queueItem.resolve(foundObject);
|
||||
|
@ -213,38 +215,23 @@ export class SavedObjectsClient {
|
|||
|
||||
}, BATCH_INTERVAL, { leading: false });
|
||||
|
||||
createSavedObject(options) {
|
||||
_createSavedObject(options) {
|
||||
return new SavedObject(this, options);
|
||||
}
|
||||
|
||||
_getUrl(path, query) {
|
||||
if (!path && !query) {
|
||||
return this._apiBaseUrl;
|
||||
_getPath(path) {
|
||||
if (!path) {
|
||||
return API_BASE_URL;
|
||||
}
|
||||
|
||||
return resolveUrl(this._apiBaseUrl, formatUrl({
|
||||
pathname: join(...path),
|
||||
query: _.pick(query, value => value != null)
|
||||
}));
|
||||
return resolveUrl(API_BASE_URL, join(...path));
|
||||
}
|
||||
|
||||
_request(method, url, body) {
|
||||
const options = { method, url, data: body };
|
||||
|
||||
_request({ method, path, query, body }) {
|
||||
if (method === 'GET' && body) {
|
||||
return this._PromiseCtor.reject(new Error('body not permitted for GET requests'));
|
||||
return Promise.reject(new Error('body not permitted for GET requests'));
|
||||
}
|
||||
|
||||
return this._$http(options)
|
||||
.then(resp => _.get(resp, 'data'))
|
||||
.catch(resp => {
|
||||
const respBody = _.get(resp, 'data', {});
|
||||
const err = new Error(respBody.message || respBody.error || `${resp.status} Response`);
|
||||
|
||||
err.statusCode = respBody.statusCode || resp.status;
|
||||
err.body = respBody;
|
||||
|
||||
throw err;
|
||||
});
|
||||
return kfetch({ method, pathname: path, query, body: JSON.stringify(body) });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,22 +17,35 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ErrorAutoCreateIndexProvider } from '../error_auto_create_index';
|
||||
import chrome from '../chrome';
|
||||
|
||||
import { SavedObjectsClient } from './saved_objects_client';
|
||||
// Provide an angular wrapper around savedObjectClient so all actions get resolved in an Angular Promise
|
||||
// If you do not need the promise to execute in an angular digest cycle then you should not use this
|
||||
// and get savedObjectClient directly from chrome.
|
||||
export function SavedObjectsClientProvider(Promise) {
|
||||
const savedObjectsClient = chrome.getSavedObjectsClient();
|
||||
|
||||
export function SavedObjectsClientProvider($http, $q, Private) {
|
||||
const errorAutoCreateIndex = Private(ErrorAutoCreateIndexProvider);
|
||||
|
||||
return new SavedObjectsClient({
|
||||
$http,
|
||||
PromiseConstructor: $q,
|
||||
onCreateFailure(error) {
|
||||
if (errorAutoCreateIndex.test(error)) {
|
||||
return errorAutoCreateIndex.takeover();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
return {
|
||||
create: (...args) => {
|
||||
return Promise.resolve(savedObjectsClient.create(...args));
|
||||
},
|
||||
bulkCreate: (...args) => {
|
||||
return Promise.resolve(savedObjectsClient.bulkCreate(...args));
|
||||
},
|
||||
delete: (...args) => {
|
||||
return Promise.resolve(savedObjectsClient.delete(...args));
|
||||
},
|
||||
find: (...args) => {
|
||||
return Promise.resolve(savedObjectsClient.find(...args));
|
||||
},
|
||||
get: (...args) => {
|
||||
return Promise.resolve(savedObjectsClient.get(...args));
|
||||
},
|
||||
bulkGet: (...args) => {
|
||||
return Promise.resolve(savedObjectsClient.bulkGet(...args));
|
||||
},
|
||||
update: (...args) => {
|
||||
return Promise.resolve(savedObjectsClient.update(...args));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -118,6 +118,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
|
@ -150,6 +151,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_saved_search.json'));
|
||||
// Wait for all the saves to happen
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.settings.clickImportDone();
|
||||
|
||||
// Second, we need to delete the index pattern
|
||||
await PageObjects.settings.navigateTo();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue