mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[uiSettings] make service request based (#12243)
* [server/uiSettings] make uiSettings service request based * [server/uiSettings] disambiguate UiSettings/Service * [server/uiSettings] link to PR in removal error * [server/uiSettings] await _read before hydrating * [server/uiSettings] focus tests, remove server integration * [server/uiSettings] add tests for readInterceptor() arg * [server/uiSettings] add server integration tests * [server/uiExports] fix replaceInjectedVars tests * [server/uiSettings] convert all methods to use async/await * [uiSettings/serviceFactory] fix doc block * [uiSettings/service] fix doc block * [uiSettings/tests/callClusterStub] stop tracking state needlessly * [uiSettings/tests] remove invalid tests and pointless promise helpers * [uiSettings/forRequest] fix typo * [uiSettings/tests] remove mixture of arrow and function expressions * [uiSettings/tests/callClusterStub] leverage sinon.calledWithExactly * [uiSettings/mixin/tests] add exception for eslint import/no-duplicates * [uiSettings/mixin/tests] wrap single args in parens
This commit is contained in:
parent
dd072c8596
commit
65d6b5d309
19 changed files with 879 additions and 639 deletions
|
@ -6,11 +6,12 @@ export default function registerDelete(server) {
|
|||
method: 'DELETE',
|
||||
handler: function (req, reply) {
|
||||
const { key } = req.params;
|
||||
const uiSettings = server.uiSettings();
|
||||
const uiSettings = req.getUiSettingsService();
|
||||
|
||||
uiSettings
|
||||
.remove(req, key)
|
||||
.remove(key)
|
||||
.then(() => uiSettings
|
||||
.getUserProvided(req)
|
||||
.getUserProvided()
|
||||
.then(settings => reply({ settings }).type('application/json'))
|
||||
)
|
||||
.catch(err => reply(Boom.wrap(err, err.statusCode)));
|
||||
|
|
|
@ -5,9 +5,9 @@ export default function registerGet(server) {
|
|||
path: '/api/kibana/settings',
|
||||
method: 'GET',
|
||||
handler: function (req, reply) {
|
||||
server
|
||||
.uiSettings()
|
||||
.getUserProvided(req)
|
||||
req
|
||||
.getUiSettingsService()
|
||||
.getUserProvided()
|
||||
.then(settings => reply({ settings }).type('application/json'))
|
||||
.catch(err => reply(Boom.wrap(err, err.statusCode)));
|
||||
}
|
||||
|
|
|
@ -7,11 +7,12 @@ export default function registerSet(server) {
|
|||
handler: function (req, reply) {
|
||||
const { key } = req.params;
|
||||
const { value } = req.payload;
|
||||
const uiSettings = server.uiSettings();
|
||||
const uiSettings = req.getUiSettingsService();
|
||||
|
||||
uiSettings
|
||||
.set(req, key, value)
|
||||
.set(key, value)
|
||||
.then(() => uiSettings
|
||||
.getUserProvided(req)
|
||||
.getUserProvided()
|
||||
.then(settings => reply({ settings }).type('application/json'))
|
||||
)
|
||||
.catch(err => reply(Boom.wrap(err, err.statusCode)));
|
||||
|
|
|
@ -6,11 +6,12 @@ export default function registerSet(server) {
|
|||
method: 'POST',
|
||||
handler: function (req, reply) {
|
||||
const { changes } = req.payload;
|
||||
const uiSettings = server.uiSettings();
|
||||
const uiSettings = req.getUiSettingsService();
|
||||
|
||||
uiSettings
|
||||
.setMany(req, changes)
|
||||
.setMany(changes)
|
||||
.then(() => uiSettings
|
||||
.getUserProvided(req)
|
||||
.getUserProvided()
|
||||
.then(settings => reply({ settings }).type('application/json'))
|
||||
)
|
||||
.catch(err => reply(Boom.wrap(err, err.statusCode)));
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function (server) {
|
|||
path: '/api/timelion/run',
|
||||
handler: async (request, reply) => {
|
||||
try {
|
||||
const uiSettings = await server.uiSettings().getAll(request);
|
||||
const uiSettings = await request.getUiSettingsService().getAll();
|
||||
|
||||
const tlConfig = require('../handlers/lib/tl_config.js')({
|
||||
server,
|
||||
|
|
|
@ -3,7 +3,7 @@ export default function (server) {
|
|||
method: 'GET',
|
||||
path: '/api/timelion/validate/es',
|
||||
handler: function (request, reply) {
|
||||
return server.uiSettings().getAll(request).then((uiSettings) => {
|
||||
return request.getUiSettingsService().getAll().then((uiSettings) => {
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
|
||||
|
||||
const timefield = uiSettings['timelion:es.timefield'];
|
||||
|
|
|
@ -120,8 +120,8 @@ export default async function (kbnServer, server, config) {
|
|||
const url = await shortUrlLookup.getUrl(request.params.urlId, request);
|
||||
shortUrlAssertValid(url);
|
||||
|
||||
const uiSettings = server.uiSettings();
|
||||
const stateStoreInSessionStorage = await uiSettings.get(request, 'state:storeInSessionStorage');
|
||||
const uiSettings = request.getUiSettingsService();
|
||||
const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage');
|
||||
if (!stateStoreInSessionStorage) {
|
||||
reply().redirect(config.get('server.basePath') + url);
|
||||
return;
|
||||
|
|
|
@ -40,7 +40,7 @@ describe('UiExports', function () {
|
|||
|
||||
await kbnServer.ready();
|
||||
kbnServer.status.get('ui settings').state = 'green';
|
||||
kbnServer.server.decorate('server', 'uiSettings', () => {
|
||||
kbnServer.server.decorate('request', 'getUiSettingsService', () => {
|
||||
return { getDefaults: noop, getUserProvided: noop };
|
||||
});
|
||||
});
|
||||
|
|
|
@ -66,7 +66,7 @@ export default async (kbnServer, server, config) => {
|
|||
});
|
||||
|
||||
async function getKibanaPayload({ app, request, includeUserProvidedConfig, injectedVarsOverrides }) {
|
||||
const uiSettings = server.uiSettings();
|
||||
const uiSettings = request.getUiSettingsService();
|
||||
const translations = await uiI18n.getTranslationsForRequest(request);
|
||||
|
||||
return {
|
||||
|
@ -82,7 +82,7 @@ export default async (kbnServer, server, config) => {
|
|||
translations: translations,
|
||||
uiSettings: await props({
|
||||
defaults: uiSettings.getDefaults(),
|
||||
user: includeUserProvidedConfig && uiSettings.getUserProvided(request)
|
||||
user: includeUserProvidedConfig && uiSettings.getUserProvided()
|
||||
}),
|
||||
vars: await reduceAsync(
|
||||
uiExports.injectedVarsReplacers,
|
||||
|
|
|
@ -1,497 +0,0 @@
|
|||
import { isEqual } from 'lodash';
|
||||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
import { uiSettingsMixin } from '../ui_settings_mixin';
|
||||
import { getDefaultSettings } from '../defaults';
|
||||
import { errors as esErrors } from 'elasticsearch';
|
||||
|
||||
async function expectRejection(promise, errorMessageContain) {
|
||||
if (!promise || typeof promise.then !== 'function') {
|
||||
throw new Error('Expected function to return a promise');
|
||||
}
|
||||
|
||||
try {
|
||||
await promise;
|
||||
} catch (err) {
|
||||
expect(err.message).to.contain(errorMessageContain);
|
||||
}
|
||||
}
|
||||
|
||||
describe('ui settings', function () {
|
||||
describe('overview', function () {
|
||||
it('has expected api surface', function () {
|
||||
const { uiSettings } = instantiate();
|
||||
expect(typeof uiSettings.get).to.equal('function');
|
||||
expect(typeof uiSettings.getAll).to.equal('function');
|
||||
expect(typeof uiSettings.getDefaults).to.equal('function');
|
||||
expect(typeof uiSettings.getRaw).to.equal('function');
|
||||
expect(typeof uiSettings.getUserProvided).to.equal('function');
|
||||
expect(typeof uiSettings.remove).to.equal('function');
|
||||
expect(typeof uiSettings.removeMany).to.equal('function');
|
||||
expect(typeof uiSettings.set).to.equal('function');
|
||||
expect(typeof uiSettings.setMany).to.equal('function');
|
||||
});
|
||||
|
||||
it('throws if the first error is not a request', async () => {
|
||||
const { uiSettings } = instantiate();
|
||||
await expectRejection(uiSettings.get(null), 'hapi.Request');
|
||||
await expectRejection(uiSettings.get(false), 'hapi.Request');
|
||||
await expectRejection(uiSettings.get('key'), 'hapi.Request');
|
||||
await expectRejection(uiSettings.get(/regex/), 'hapi.Request');
|
||||
await expectRejection(uiSettings.get(new Date()), 'hapi.Request');
|
||||
await expectRejection(uiSettings.get({}), 'hapi.Request');
|
||||
await expectRejection(uiSettings.get({ path:'' }), 'hapi.Request');
|
||||
await expectRejection(uiSettings.get({ path:'', headers:null }), 'hapi.Request');
|
||||
await expectRejection(uiSettings.get({ headers:{} }), 'hapi.Request');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setMany()', function () {
|
||||
it('returns a promise', () => {
|
||||
const { uiSettings, req } = instantiate();
|
||||
const result = uiSettings.setMany(req, { a: 'b' });
|
||||
expect(result).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('updates a single value in one operation', function () {
|
||||
const { server, uiSettings, configGet, req } = instantiate();
|
||||
uiSettings.setMany(req, { one: 'value' });
|
||||
expectElasticsearchUpdateQuery(server, req, configGet, {
|
||||
one: 'value'
|
||||
});
|
||||
});
|
||||
|
||||
it('updates several values in one operation', function () {
|
||||
const { server, uiSettings, configGet, req } = instantiate();
|
||||
uiSettings.setMany(req, { one: 'value', another: 'val' });
|
||||
expectElasticsearchUpdateQuery(server, req, configGet, {
|
||||
one: 'value', another: 'val'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#set()', function () {
|
||||
it('returns a promise', () => {
|
||||
const { uiSettings, req } = instantiate();
|
||||
const result = uiSettings.set(req, 'a', 'b');
|
||||
expect(result).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('updates single values by (key, value)', function () {
|
||||
const { server, uiSettings, configGet, req } = instantiate();
|
||||
uiSettings.set(req, 'one', 'value');
|
||||
expectElasticsearchUpdateQuery(server, req, configGet, {
|
||||
one: 'value'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#remove()', function () {
|
||||
it('returns a promise', () => {
|
||||
const { uiSettings, req } = instantiate();
|
||||
const result = uiSettings.remove(req, 'one');
|
||||
expect(result).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('removes single values by key', function () {
|
||||
const { server, uiSettings, configGet, req } = instantiate();
|
||||
uiSettings.remove(req, 'one');
|
||||
expectElasticsearchUpdateQuery(server, req, configGet, {
|
||||
one: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeMany()', function () {
|
||||
it('returns a promise', () => {
|
||||
const { uiSettings, req } = instantiate();
|
||||
const result = uiSettings.removeMany(req, ['one']);
|
||||
expect(result).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('removes a single value', function () {
|
||||
const { server, uiSettings, configGet, req } = instantiate();
|
||||
uiSettings.removeMany(req, ['one']);
|
||||
expectElasticsearchUpdateQuery(server, req, configGet, {
|
||||
one: null
|
||||
});
|
||||
});
|
||||
|
||||
it('updates several values in one operation', function () {
|
||||
const { server, uiSettings, configGet, req } = instantiate();
|
||||
uiSettings.removeMany(req, ['one', 'two', 'three']);
|
||||
expectElasticsearchUpdateQuery(server, req, configGet, {
|
||||
one: null, two: null, three: null
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDefaults()', function () {
|
||||
it('is promised the default values', async function () {
|
||||
const {
|
||||
uiSettings
|
||||
} = instantiate();
|
||||
const defaults = await uiSettings.getDefaults();
|
||||
expect(isEqual(defaults, getDefaultSettings())).to.equal(true);
|
||||
});
|
||||
|
||||
|
||||
describe('defaults for formatters', async function () {
|
||||
|
||||
const defaults = getDefaultSettings();
|
||||
const mapping = JSON.parse(defaults['format:defaultTypeMap'].value);
|
||||
const expected = {
|
||||
ip: { id: 'ip', params: {} },
|
||||
date: { id: 'date', params: {} },
|
||||
number: { id: 'number', params: {} },
|
||||
boolean: { id: 'boolean', params: {} },
|
||||
_source: { id: '_source', params: {} },
|
||||
_default_: { id: 'string', params: {} }
|
||||
};
|
||||
|
||||
Object.keys(mapping).forEach(function (dataType) {
|
||||
it(`should configure ${dataType}`, function () {
|
||||
expect(expected.hasOwnProperty(dataType)).to.equal(true);
|
||||
expect(mapping[dataType].id).to.equal(expected[dataType].id);
|
||||
expect(JSON.stringify(mapping[dataType].params)).to.equal(JSON.stringify(expected[dataType].params));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserProvided()', function () {
|
||||
it('pulls user configuration from ES', async function () {
|
||||
const getResult = { user: 'customized' };
|
||||
const { server, uiSettings, configGet, req } = instantiate({ getResult });
|
||||
await uiSettings.getUserProvided(req);
|
||||
expectElasticsearchGetQuery(server, req, configGet);
|
||||
});
|
||||
|
||||
it('returns user configuration', async function () {
|
||||
const getResult = { user: 'customized' };
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.getUserProvided(req);
|
||||
expect(isEqual(result, {
|
||||
user: { userValue: 'customized' }
|
||||
})).to.equal(true);
|
||||
});
|
||||
|
||||
it('ignores null user configuration (because default values)', async function () {
|
||||
const getResult = { user: 'customized', usingDefault: null, something: 'else' };
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.getUserProvided(req);
|
||||
expect(isEqual(result, {
|
||||
user: { userValue: 'customized' }, something: { userValue: 'else' }
|
||||
})).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns an empty object when status is not green', async function () {
|
||||
const { uiSettings, req } = instantiate({
|
||||
settingsStatusOverrides: { state: 'yellow' }
|
||||
});
|
||||
|
||||
expect(await uiSettings.getUserProvided(req)).to.eql({});
|
||||
});
|
||||
|
||||
it('returns an empty object on 404 responses', async function () {
|
||||
const { uiSettings, req } = instantiate({
|
||||
async callWithRequest() {
|
||||
throw new esErrors[404]();
|
||||
}
|
||||
});
|
||||
|
||||
expect(await uiSettings.getUserProvided(req)).to.eql({});
|
||||
});
|
||||
|
||||
it('returns an empty object on 403 responses', async function () {
|
||||
const { uiSettings, req } = instantiate({
|
||||
async callWithRequest() {
|
||||
throw new esErrors[403]();
|
||||
}
|
||||
});
|
||||
|
||||
expect(await uiSettings.getUserProvided(req)).to.eql({});
|
||||
});
|
||||
|
||||
it('returns an empty object on NoConnections responses', async function () {
|
||||
const { uiSettings, req } = instantiate({
|
||||
async callWithRequest() {
|
||||
throw new esErrors.NoConnections();
|
||||
}
|
||||
});
|
||||
|
||||
expect(await uiSettings.getUserProvided(req)).to.eql({});
|
||||
});
|
||||
|
||||
it('throws 401 errors', async function () {
|
||||
const { uiSettings, req } = instantiate({
|
||||
async callWithRequest() {
|
||||
throw new esErrors[401]();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await uiSettings.getUserProvided(req);
|
||||
throw new Error('expect getUserProvided() to throw');
|
||||
} catch (err) {
|
||||
expect(err).to.be.a(esErrors[401]);
|
||||
}
|
||||
});
|
||||
|
||||
it('throw when callWithRequest fails in some unexpected way', async function () {
|
||||
const expectedUnexpectedError = new Error('unexpected');
|
||||
|
||||
const { uiSettings, req } = instantiate({
|
||||
async callWithRequest() {
|
||||
throw expectedUnexpectedError;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await uiSettings.getUserProvided(req);
|
||||
throw new Error('expect getUserProvided() to throw');
|
||||
} catch (err) {
|
||||
expect(err).to.be(expectedUnexpectedError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getRaw()', function () {
|
||||
it('pulls user configuration from ES', async function () {
|
||||
const getResult = {};
|
||||
const { server, uiSettings, configGet, req } = instantiate({ getResult });
|
||||
await uiSettings.getRaw(req);
|
||||
expectElasticsearchGetQuery(server, req, configGet);
|
||||
});
|
||||
|
||||
it(`without user configuration it's equal to the defaults`, async function () {
|
||||
const getResult = {};
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.getRaw(req);
|
||||
expect(isEqual(result, getDefaultSettings())).to.equal(true);
|
||||
});
|
||||
|
||||
it(`user configuration gets merged with defaults`, async function () {
|
||||
const getResult = { foo: 'bar' };
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.getRaw(req);
|
||||
const merged = getDefaultSettings();
|
||||
merged.foo = { userValue: 'bar' };
|
||||
expect(isEqual(result, merged)).to.equal(true);
|
||||
});
|
||||
|
||||
it(`user configuration gets merged into defaults`, async function () {
|
||||
const getResult = { dateFormat: 'YYYY-MM-DD' };
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.getRaw(req);
|
||||
const merged = getDefaultSettings();
|
||||
merged.dateFormat.userValue = 'YYYY-MM-DD';
|
||||
expect(isEqual(result, merged)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAll()', function () {
|
||||
it('pulls user configuration from ES', async function () {
|
||||
const getResult = {};
|
||||
const { server, uiSettings, configGet, req } = instantiate({ getResult });
|
||||
await uiSettings.getAll(req);
|
||||
expectElasticsearchGetQuery(server, req, configGet);
|
||||
});
|
||||
|
||||
it(`returns key value pairs`, async function () {
|
||||
const getResult = {};
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.getAll(req);
|
||||
const defaults = getDefaultSettings();
|
||||
const expectation = {};
|
||||
Object.keys(defaults).forEach(key => {
|
||||
expectation[key] = defaults[key].value;
|
||||
});
|
||||
expect(isEqual(result, expectation)).to.equal(true);
|
||||
});
|
||||
|
||||
it(`returns key value pairs including user configuration`, async function () {
|
||||
const getResult = { something: 'user-provided' };
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.getAll(req);
|
||||
const defaults = getDefaultSettings();
|
||||
const expectation = {};
|
||||
Object.keys(defaults).forEach(key => {
|
||||
expectation[key] = defaults[key].value;
|
||||
});
|
||||
expectation.something = 'user-provided';
|
||||
expect(isEqual(result, expectation)).to.equal(true);
|
||||
});
|
||||
|
||||
it(`returns key value pairs including user configuration for existing settings`, async function () {
|
||||
const getResult = { dateFormat: 'YYYY-MM-DD' };
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.getAll(req);
|
||||
const defaults = getDefaultSettings();
|
||||
const expectation = {};
|
||||
Object.keys(defaults).forEach(key => {
|
||||
expectation[key] = defaults[key].value;
|
||||
});
|
||||
expectation.dateFormat = 'YYYY-MM-DD';
|
||||
expect(isEqual(result, expectation)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get()', function () {
|
||||
it('pulls user configuration from ES', async function () {
|
||||
const getResult = {};
|
||||
const { server, uiSettings, configGet, req } = instantiate({ getResult });
|
||||
await uiSettings.get(req);
|
||||
expectElasticsearchGetQuery(server, req, configGet);
|
||||
});
|
||||
|
||||
it(`returns the promised value for a key`, async function () {
|
||||
const getResult = {};
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.get(req, 'dateFormat');
|
||||
const defaults = getDefaultSettings();
|
||||
expect(result).to.equal(defaults.dateFormat.value);
|
||||
});
|
||||
|
||||
it(`returns the user-configured value for a custom key`, async function () {
|
||||
const getResult = { custom: 'value' };
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.get(req, 'custom');
|
||||
expect(result).to.equal('value');
|
||||
});
|
||||
|
||||
it(`returns the user-configured value for a modified key`, async function () {
|
||||
const getResult = { dateFormat: 'YYYY-MM-DD' };
|
||||
const {
|
||||
uiSettings,
|
||||
req
|
||||
} = instantiate({ getResult });
|
||||
const result = await uiSettings.get(req, 'dateFormat');
|
||||
expect(result).to.equal('YYYY-MM-DD');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectElasticsearchGetQuery(server, req, configGet) {
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
const [reqPassed, method, params] = callWithRequest.args[0];
|
||||
expect(reqPassed).to.be(req);
|
||||
expect(method).to.be('get');
|
||||
expect(params).to.eql({
|
||||
index: configGet('kibana.index'),
|
||||
id: configGet('pkg.version'),
|
||||
type: 'config'
|
||||
});
|
||||
}
|
||||
|
||||
function expectElasticsearchUpdateQuery(server, req, configGet, doc) {
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
|
||||
sinon.assert.calledOnce(callWithRequest);
|
||||
const [reqPassed, method, params] = callWithRequest.args[0];
|
||||
expect(reqPassed).to.be(req);
|
||||
expect(method).to.be('update');
|
||||
expect(params).to.eql({
|
||||
index: configGet('kibana.index'),
|
||||
id: configGet('pkg.version'),
|
||||
type: 'config',
|
||||
body: { doc }
|
||||
});
|
||||
}
|
||||
|
||||
function instantiate({ getResult, callWithRequest, settingsStatusOverrides } = {}) {
|
||||
const esStatus = {
|
||||
state: 'green',
|
||||
on: sinon.spy()
|
||||
};
|
||||
const settingsStatus = {
|
||||
state: 'green',
|
||||
red: sinon.spy(),
|
||||
yellow: sinon.spy(),
|
||||
green: sinon.spy(),
|
||||
...settingsStatusOverrides
|
||||
};
|
||||
const kbnServer = {
|
||||
status: {
|
||||
create: sinon.stub().withArgs('ui settings').returns(settingsStatus),
|
||||
getForPluginId: sinon.stub().withArgs('elasticsearch').returns(esStatus)
|
||||
},
|
||||
ready: sinon.stub().returns(Promise.resolve())
|
||||
};
|
||||
|
||||
const req = { __stubHapiRequest: true, path: '', headers: {} };
|
||||
|
||||
const adminCluster = {
|
||||
errors: esErrors,
|
||||
callWithInternalUser: sinon.stub(),
|
||||
callWithRequest: sinon.spy((withReq, method, params) => {
|
||||
if (callWithRequest) {
|
||||
return callWithRequest(withReq, method, params);
|
||||
}
|
||||
|
||||
expect(withReq).to.be(req);
|
||||
switch (method) {
|
||||
case 'get':
|
||||
return Promise.resolve({ _source: getResult });
|
||||
case 'update':
|
||||
return Promise.resolve();
|
||||
default:
|
||||
throw new Error(`callWithRequest() is using unexpected method "${method}"`);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
adminCluster.callWithInternalUser.withArgs('get', sinon.match.any).returns(Promise.resolve({ _source: getResult }));
|
||||
adminCluster.callWithInternalUser.withArgs('update', sinon.match.any).returns(Promise.resolve());
|
||||
|
||||
const configGet = sinon.stub();
|
||||
configGet.withArgs('kibana.index').returns('.kibana');
|
||||
configGet.withArgs('pkg.version').returns('1.2.3-test');
|
||||
configGet.withArgs('uiSettings.enabled').returns(true);
|
||||
const config = {
|
||||
get: configGet
|
||||
};
|
||||
|
||||
const server = {
|
||||
config: () => config,
|
||||
decorate: (_, key, value) => server[key] = value,
|
||||
plugins: {
|
||||
elasticsearch: {
|
||||
getCluster: sinon.stub().withArgs('admin').returns(adminCluster)
|
||||
}
|
||||
}
|
||||
};
|
||||
uiSettingsMixin(kbnServer, server, config);
|
||||
const uiSettings = server.uiSettings();
|
||||
return { server, uiSettings, configGet, req };
|
||||
}
|
41
src/ui/ui_settings/__tests__/lib/call_cluster_stub.js
Normal file
41
src/ui/ui_settings/__tests__/lib/call_cluster_stub.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
|
||||
export function createCallClusterStub(index, type, id, esDocSource) {
|
||||
const callCluster = sinon.spy(async (method, params) => {
|
||||
expect(params)
|
||||
.to.have.property('index', index)
|
||||
.and.to.have.property('type', type)
|
||||
.and.to.have.property('id', id);
|
||||
|
||||
switch (method) {
|
||||
case 'get':
|
||||
return { _source: { ...esDocSource } };
|
||||
|
||||
case 'update':
|
||||
expect(params).to.have.property('body');
|
||||
expect(params.body).to.have.property('doc');
|
||||
return {};
|
||||
|
||||
default:
|
||||
throw new Error(`unexpected es method ${method}`);
|
||||
}
|
||||
});
|
||||
|
||||
callCluster.assertGetQuery = () => {
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
sinon.assert.calledWith(callCluster, 'get');
|
||||
};
|
||||
|
||||
callCluster.assertUpdateQuery = doc => {
|
||||
sinon.assert.calledOnce(callCluster);
|
||||
sinon.assert.calledWithExactly(callCluster, 'update', {
|
||||
index,
|
||||
type,
|
||||
id,
|
||||
body: { doc }
|
||||
});
|
||||
};
|
||||
|
||||
return callCluster;
|
||||
}
|
1
src/ui/ui_settings/__tests__/lib/index.js
Normal file
1
src/ui/ui_settings/__tests__/lib/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { createCallClusterStub } from './call_cluster_stub';
|
197
src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js
Normal file
197
src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
import Chance from 'chance';
|
||||
|
||||
import ServerStatus from '../../../server/status/server_status';
|
||||
import Config from '../../../server/config/config';
|
||||
|
||||
/* eslint-disable import/no-duplicates */
|
||||
import * as uiSettingsServiceFactoryNS from '../ui_settings_service_factory';
|
||||
import { uiSettingsServiceFactory } from '../ui_settings_service_factory';
|
||||
import * as getUiSettingsServiceForRequestNS from '../ui_settings_service_for_request';
|
||||
import { getUiSettingsServiceForRequest } from '../ui_settings_service_for_request';
|
||||
/* eslint-enable import/no-duplicates */
|
||||
|
||||
import { uiSettingsMixin } from '../ui_settings_mixin';
|
||||
|
||||
const chance = new Chance();
|
||||
|
||||
describe('uiSettingsMixin()', () => {
|
||||
const sandbox = sinon.sandbox.create();
|
||||
|
||||
function setup(options = {}) {
|
||||
const {
|
||||
enabled = true
|
||||
} = options;
|
||||
|
||||
const config = Config.withDefaultSchema({
|
||||
uiSettings: { enabled }
|
||||
});
|
||||
|
||||
// maps of decorations passed to `server.decorate()`
|
||||
const decorations = {
|
||||
server: {},
|
||||
request: {}
|
||||
};
|
||||
|
||||
// mock hapi server
|
||||
const server = {
|
||||
log: sinon.stub(),
|
||||
config: () => config,
|
||||
decorate: sinon.spy((type, name, value) => {
|
||||
decorations[type][name] = value;
|
||||
}),
|
||||
};
|
||||
|
||||
// "promise" returned from kbnServer.ready()
|
||||
const readyPromise = {
|
||||
then: sinon.stub(),
|
||||
};
|
||||
|
||||
const kbnServer = {
|
||||
server,
|
||||
config,
|
||||
status: new ServerStatus(server),
|
||||
ready: sinon.stub().returns(readyPromise),
|
||||
};
|
||||
|
||||
uiSettingsMixin(kbnServer, server, config);
|
||||
|
||||
return {
|
||||
kbnServer,
|
||||
server,
|
||||
decorations,
|
||||
readyPromise,
|
||||
status: kbnServer.status.get('ui settings'),
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
describe('status', () => {
|
||||
it('creates a "ui settings" status', () => {
|
||||
const { status } = setup();
|
||||
expect(status).to.have.property('state', 'uninitialized');
|
||||
});
|
||||
|
||||
describe('disabled', () => {
|
||||
it('disables if uiSettings.enabled config is false', () => {
|
||||
const { status } = setup({ enabled: false });
|
||||
expect(status).to.have.property('state', 'disabled');
|
||||
});
|
||||
|
||||
it('does not register a handler for kbnServer.ready()', () => {
|
||||
const { readyPromise } = setup({ enabled: false });
|
||||
sinon.assert.notCalled(readyPromise.then);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enabled', () => {
|
||||
it('registers a handler for kbnServer.ready()', () => {
|
||||
const { readyPromise } = setup();
|
||||
sinon.assert.calledOnce(readyPromise.then);
|
||||
});
|
||||
|
||||
it('mirrors the elasticsearch plugin status once kibanaServer.ready() resolves', () => {
|
||||
const { kbnServer, readyPromise, status } = setup();
|
||||
const esStatus = kbnServer.status.createForPlugin({
|
||||
id: 'elasticsearch',
|
||||
version: 'kibana',
|
||||
});
|
||||
|
||||
esStatus.green();
|
||||
expect(status).to.have.property('state', 'uninitialized');
|
||||
const readyPromiseHandler = readyPromise.then.firstCall.args[0];
|
||||
readyPromiseHandler();
|
||||
expect(status).to.have.property('state', 'green');
|
||||
|
||||
|
||||
const states = chance.shuffle(['red', 'green', 'yellow']);
|
||||
states.forEach((state) => {
|
||||
esStatus[state]();
|
||||
expect(esStatus).to.have.property('state', state);
|
||||
expect(status).to.have.property('state', state);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('server.uiSettingsServiceFactory()', () => {
|
||||
it('decorates server with "uiSettingsServiceFactory"', () => {
|
||||
const { decorations } = setup();
|
||||
expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function');
|
||||
|
||||
sandbox.stub(uiSettingsServiceFactoryNS, 'uiSettingsServiceFactory');
|
||||
sinon.assert.notCalled(uiSettingsServiceFactory);
|
||||
decorations.server.uiSettingsServiceFactory();
|
||||
sinon.assert.calledOnce(uiSettingsServiceFactory);
|
||||
});
|
||||
|
||||
it('passes `server` and `options` argument to factory', () => {
|
||||
const { decorations, server } = setup();
|
||||
expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function');
|
||||
|
||||
sandbox.stub(uiSettingsServiceFactoryNS, 'uiSettingsServiceFactory');
|
||||
sinon.assert.notCalled(uiSettingsServiceFactory);
|
||||
const football = {};
|
||||
decorations.server.uiSettingsServiceFactory(football);
|
||||
sinon.assert.calledWith(uiSettingsServiceFactory, server, football);
|
||||
});
|
||||
});
|
||||
|
||||
describe('request.getUiSettingsService()', () => {
|
||||
it('exposes "getUiSettingsService" on requests', () => {
|
||||
const { decorations } = setup();
|
||||
expect(decorations.request).to.have.property('getUiSettingsService').a('function');
|
||||
|
||||
sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest');
|
||||
sinon.assert.notCalled(getUiSettingsServiceForRequest);
|
||||
decorations.request.getUiSettingsService();
|
||||
sinon.assert.calledOnce(getUiSettingsServiceForRequest);
|
||||
});
|
||||
|
||||
it('passes request to getUiSettingsServiceForRequest', () => {
|
||||
const { server, decorations } = setup();
|
||||
expect(decorations.request).to.have.property('getUiSettingsService').a('function');
|
||||
|
||||
sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest');
|
||||
sinon.assert.notCalled(getUiSettingsServiceForRequest);
|
||||
const request = {};
|
||||
decorations.request.getUiSettingsService.call(request);
|
||||
sinon.assert.calledWith(getUiSettingsServiceForRequest, server, request);
|
||||
});
|
||||
|
||||
it('defines read interceptor that intercepts when status is not green', () => {
|
||||
const { status, decorations } = setup();
|
||||
expect(decorations.request).to.have.property('getUiSettingsService').a('function');
|
||||
|
||||
sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest');
|
||||
decorations.request.getUiSettingsService();
|
||||
|
||||
const readInterceptor = getUiSettingsServiceForRequest.firstCall.args[2];
|
||||
expect(readInterceptor).to.be.a('function');
|
||||
|
||||
status.green();
|
||||
expect(readInterceptor()).to.be(undefined);
|
||||
|
||||
status.yellow();
|
||||
expect(readInterceptor()).to.eql({});
|
||||
|
||||
status.red();
|
||||
expect(readInterceptor()).to.eql({});
|
||||
|
||||
status.green();
|
||||
expect(readInterceptor()).to.eql(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('server.uiSettings()', () => {
|
||||
it('throws an error, links to pr', () => {
|
||||
const { decorations } = setup();
|
||||
expect(decorations.server).to.have.property('uiSettings').a('function');
|
||||
expect(() => {
|
||||
decorations.server.uiSettings();
|
||||
}).to.throwError('http://github.com');
|
||||
});
|
||||
});
|
||||
});
|
393
src/ui/ui_settings/__tests__/ui_settings_service.js
Normal file
393
src/ui/ui_settings/__tests__/ui_settings_service.js
Normal file
|
@ -0,0 +1,393 @@
|
|||
import { isEqual } from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import { errors as esErrors } from 'elasticsearch';
|
||||
|
||||
import { getDefaultSettings } from '../defaults';
|
||||
import { UiSettingsService } from '../ui_settings_service';
|
||||
|
||||
import { createCallClusterStub } from './lib';
|
||||
|
||||
const INDEX = '.kibana';
|
||||
const TYPE = 'config';
|
||||
const ID = 'kibana-version';
|
||||
|
||||
function setup(options = {}) {
|
||||
const {
|
||||
readInterceptor,
|
||||
esDocSource = {},
|
||||
callCluster = createCallClusterStub(INDEX, TYPE, ID, esDocSource)
|
||||
} = options;
|
||||
|
||||
const uiSettings = new UiSettingsService({
|
||||
index: INDEX,
|
||||
type: TYPE,
|
||||
id: ID,
|
||||
readInterceptor,
|
||||
callCluster,
|
||||
});
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
assertGetQuery: callCluster.assertGetQuery,
|
||||
assertUpdateQuery: callCluster.assertUpdateQuery,
|
||||
};
|
||||
}
|
||||
|
||||
describe('ui settings', () => {
|
||||
describe('overview', () => {
|
||||
it('has expected api surface', () => {
|
||||
const { uiSettings } = setup();
|
||||
expect(uiSettings).to.have.property('get').a('function');
|
||||
expect(uiSettings).to.have.property('getAll').a('function');
|
||||
expect(uiSettings).to.have.property('getDefaults').a('function');
|
||||
expect(uiSettings).to.have.property('getRaw').a('function');
|
||||
expect(uiSettings).to.have.property('getUserProvided').a('function');
|
||||
expect(uiSettings).to.have.property('remove').a('function');
|
||||
expect(uiSettings).to.have.property('removeMany').a('function');
|
||||
expect(uiSettings).to.have.property('set').a('function');
|
||||
expect(uiSettings).to.have.property('setMany').a('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setMany()', () => {
|
||||
it('returns a promise', () => {
|
||||
const { uiSettings } = setup();
|
||||
expect(uiSettings.setMany({ a: 'b' })).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('updates a single value in one operation', async () => {
|
||||
const { uiSettings, assertUpdateQuery } = setup();
|
||||
await uiSettings.setMany({ one: 'value' });
|
||||
assertUpdateQuery({ one: 'value' });
|
||||
});
|
||||
|
||||
it('updates several values in one operation', async () => {
|
||||
const { uiSettings, assertUpdateQuery } = setup();
|
||||
await uiSettings.setMany({ one: 'value', another: 'val' });
|
||||
assertUpdateQuery({ one: 'value', another: 'val' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#set()', () => {
|
||||
it('returns a promise', () => {
|
||||
const { uiSettings } = setup();
|
||||
expect(uiSettings.set('a', 'b')).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('updates single values by (key, value)', async () => {
|
||||
const { uiSettings, assertUpdateQuery } = setup();
|
||||
await uiSettings.set('one', 'value');
|
||||
assertUpdateQuery({ one: 'value' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#remove()', () => {
|
||||
it('returns a promise', () => {
|
||||
const { uiSettings } = setup();
|
||||
expect(uiSettings.remove('one')).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('removes single values by key', async () => {
|
||||
const { uiSettings, assertUpdateQuery } = setup();
|
||||
await uiSettings.remove('one');
|
||||
assertUpdateQuery({ one: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeMany()', () => {
|
||||
it('returns a promise', () => {
|
||||
const { uiSettings } = setup();
|
||||
expect(uiSettings.removeMany(['one'])).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('removes a single value', async () => {
|
||||
const { uiSettings, assertUpdateQuery } = setup();
|
||||
await uiSettings.removeMany(['one']);
|
||||
assertUpdateQuery({ one: null });
|
||||
});
|
||||
|
||||
it('updates several values in one operation', async () => {
|
||||
const { uiSettings, assertUpdateQuery } = setup();
|
||||
await uiSettings.removeMany(['one', 'two', 'three']);
|
||||
assertUpdateQuery({ one: null, two: null, three: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDefaults()', () => {
|
||||
it('is promised the default values', async () => {
|
||||
const {
|
||||
uiSettings
|
||||
} = setup();
|
||||
const defaults = await uiSettings.getDefaults();
|
||||
expect(isEqual(defaults, getDefaultSettings())).to.equal(true);
|
||||
});
|
||||
|
||||
|
||||
describe('defaults for formatters', async () => {
|
||||
|
||||
const defaults = getDefaultSettings();
|
||||
const mapping = JSON.parse(defaults['format:defaultTypeMap'].value);
|
||||
const expected = {
|
||||
ip: { id: 'ip', params: {} },
|
||||
date: { id: 'date', params: {} },
|
||||
number: { id: 'number', params: {} },
|
||||
boolean: { id: 'boolean', params: {} },
|
||||
_source: { id: '_source', params: {} },
|
||||
_default_: { id: 'string', params: {} }
|
||||
};
|
||||
|
||||
Object.keys(mapping).forEach((dataType) => {
|
||||
it(`should configure ${dataType}`, () => {
|
||||
expect(expected.hasOwnProperty(dataType)).to.equal(true);
|
||||
expect(mapping[dataType].id).to.equal(expected[dataType].id);
|
||||
expect(JSON.stringify(mapping[dataType].params)).to.equal(JSON.stringify(expected[dataType].params));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserProvided()', () => {
|
||||
it('pulls user configuration from ES', async () => {
|
||||
const { uiSettings, assertGetQuery } = setup();
|
||||
await uiSettings.getUserProvided();
|
||||
assertGetQuery();
|
||||
});
|
||||
|
||||
it('returns user configuration', async () => {
|
||||
const esDocSource = { user: 'customized' };
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.getUserProvided();
|
||||
expect(isEqual(result, {
|
||||
user: { userValue: 'customized' }
|
||||
})).to.equal(true);
|
||||
});
|
||||
|
||||
it('ignores null user configuration (because default values)', async () => {
|
||||
const esDocSource = { user: 'customized', usingDefault: null, something: 'else' };
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.getUserProvided();
|
||||
expect(isEqual(result, {
|
||||
user: { userValue: 'customized' }, something: { userValue: 'else' }
|
||||
})).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns an empty object on 404 responses', async () => {
|
||||
const { uiSettings } = setup({
|
||||
async callCluster() {
|
||||
throw new esErrors[404]();
|
||||
}
|
||||
});
|
||||
|
||||
expect(await uiSettings.getUserProvided()).to.eql({});
|
||||
});
|
||||
|
||||
it('returns an empty object on 403 responses', async () => {
|
||||
const { uiSettings } = setup({
|
||||
async callCluster() {
|
||||
throw new esErrors[403]();
|
||||
}
|
||||
});
|
||||
|
||||
expect(await uiSettings.getUserProvided()).to.eql({});
|
||||
});
|
||||
|
||||
it('returns an empty object on NoConnections responses', async () => {
|
||||
const { uiSettings } = setup({
|
||||
async callCluster() {
|
||||
throw new esErrors.NoConnections();
|
||||
}
|
||||
});
|
||||
|
||||
expect(await uiSettings.getUserProvided()).to.eql({});
|
||||
});
|
||||
|
||||
it('throws 401 errors', async () => {
|
||||
const { uiSettings } = setup({
|
||||
async callCluster() {
|
||||
throw new esErrors[401]();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await uiSettings.getUserProvided();
|
||||
throw new Error('expect getUserProvided() to throw');
|
||||
} catch (err) {
|
||||
expect(err).to.be.a(esErrors[401]);
|
||||
}
|
||||
});
|
||||
|
||||
it('throw when callCluster fails in some unexpected way', async () => {
|
||||
const expectedUnexpectedError = new Error('unexpected');
|
||||
|
||||
const { uiSettings } = setup({
|
||||
async callCluster() {
|
||||
throw expectedUnexpectedError;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await uiSettings.getUserProvided();
|
||||
throw new Error('expect getUserProvided() to throw');
|
||||
} catch (err) {
|
||||
expect(err).to.be(expectedUnexpectedError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getRaw()', () => {
|
||||
it('pulls user configuration from ES', async () => {
|
||||
const esDocSource = {};
|
||||
const { uiSettings, assertGetQuery } = setup({ esDocSource });
|
||||
await uiSettings.getRaw();
|
||||
assertGetQuery();
|
||||
});
|
||||
|
||||
it(`without user configuration it's equal to the defaults`, async () => {
|
||||
const esDocSource = {};
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.getRaw();
|
||||
expect(isEqual(result, getDefaultSettings())).to.equal(true);
|
||||
});
|
||||
|
||||
it(`user configuration gets merged with defaults`, async () => {
|
||||
const esDocSource = { foo: 'bar' };
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.getRaw();
|
||||
const merged = getDefaultSettings();
|
||||
merged.foo = { userValue: 'bar' };
|
||||
expect(isEqual(result, merged)).to.equal(true);
|
||||
});
|
||||
|
||||
it(`user configuration gets merged into defaults`, async () => {
|
||||
const esDocSource = { dateFormat: 'YYYY-MM-DD' };
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.getRaw();
|
||||
const merged = getDefaultSettings();
|
||||
merged.dateFormat.userValue = 'YYYY-MM-DD';
|
||||
expect(isEqual(result, merged)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAll()', () => {
|
||||
it('pulls user configuration from ES', async () => {
|
||||
const esDocSource = {};
|
||||
const { uiSettings, assertGetQuery } = setup({ esDocSource });
|
||||
await uiSettings.getAll();
|
||||
assertGetQuery();
|
||||
});
|
||||
|
||||
it(`returns key value pairs`, async () => {
|
||||
const esDocSource = {};
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.getAll();
|
||||
const defaults = getDefaultSettings();
|
||||
const expectation = {};
|
||||
Object.keys(defaults).forEach((key) => {
|
||||
expectation[key] = defaults[key].value;
|
||||
});
|
||||
expect(isEqual(result, expectation)).to.equal(true);
|
||||
});
|
||||
|
||||
it(`returns key value pairs including user configuration`, async () => {
|
||||
const esDocSource = { something: 'user-provided' };
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.getAll();
|
||||
const defaults = getDefaultSettings();
|
||||
const expectation = {};
|
||||
Object.keys(defaults).forEach((key) => {
|
||||
expectation[key] = defaults[key].value;
|
||||
});
|
||||
expectation.something = 'user-provided';
|
||||
expect(isEqual(result, expectation)).to.equal(true);
|
||||
});
|
||||
|
||||
it(`returns key value pairs including user configuration for existing settings`, async () => {
|
||||
const esDocSource = { dateFormat: 'YYYY-MM-DD' };
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.getAll();
|
||||
const defaults = getDefaultSettings();
|
||||
const expectation = {};
|
||||
Object.keys(defaults).forEach((key) => {
|
||||
expectation[key] = defaults[key].value;
|
||||
});
|
||||
expectation.dateFormat = 'YYYY-MM-DD';
|
||||
expect(isEqual(result, expectation)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get()', () => {
|
||||
it('pulls user configuration from ES', async () => {
|
||||
const esDocSource = {};
|
||||
const { uiSettings, assertGetQuery } = setup({ esDocSource });
|
||||
await uiSettings.get();
|
||||
assertGetQuery();
|
||||
});
|
||||
|
||||
it(`returns the promised value for a key`, async () => {
|
||||
const esDocSource = {};
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.get('dateFormat');
|
||||
const defaults = getDefaultSettings();
|
||||
expect(result).to.equal(defaults.dateFormat.value);
|
||||
});
|
||||
|
||||
it(`returns the user-configured value for a custom key`, async () => {
|
||||
const esDocSource = { custom: 'value' };
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.get('custom');
|
||||
expect(result).to.equal('value');
|
||||
});
|
||||
|
||||
it(`returns the user-configured value for a modified key`, async () => {
|
||||
const esDocSource = { dateFormat: 'YYYY-MM-DD' };
|
||||
const { uiSettings } = setup({ esDocSource });
|
||||
const result = await uiSettings.get('dateFormat');
|
||||
expect(result).to.equal('YYYY-MM-DD');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readInterceptor() argument', () => {
|
||||
describe('#getUserProvided()', () => {
|
||||
it('returns a promise when interceptValue doesn\'t', () => {
|
||||
const { uiSettings } = setup({ readInterceptor: () => ({}) });
|
||||
expect(uiSettings.getUserProvided()).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('returns intercept values', async () => {
|
||||
const { uiSettings } = setup({
|
||||
readInterceptor: () => ({
|
||||
foo: 'bar'
|
||||
})
|
||||
});
|
||||
|
||||
expect(await uiSettings.getUserProvided()).to.eql({
|
||||
foo: {
|
||||
userValue: 'bar'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAll()', () => {
|
||||
it('merges intercept value with defaults', async () => {
|
||||
const { uiSettings } = setup({
|
||||
readInterceptor: () => ({
|
||||
foo: 'not foo'
|
||||
}),
|
||||
});
|
||||
|
||||
const defaults = getDefaultSettings();
|
||||
const defaultValues = Object.keys(defaults).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: defaults[key].value,
|
||||
}), {});
|
||||
|
||||
expect(await uiSettings.getAll()).to.eql({
|
||||
...defaultValues,
|
||||
foo: 'not foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -1,120 +0,0 @@
|
|||
import { defaultsDeep } from 'lodash';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
import { getDefaultSettings } from './defaults';
|
||||
|
||||
function hydrateUserSettings(user) {
|
||||
return Object.keys(user)
|
||||
.map(key => ({ key, userValue: user[key] }))
|
||||
.filter(({ userValue }) => userValue !== null)
|
||||
.reduce((acc, { key, userValue }) => ({ ...acc, [key]: { userValue } }), {});
|
||||
}
|
||||
|
||||
function assertRequest(req) {
|
||||
if (
|
||||
!req ||
|
||||
typeof req !== 'object' ||
|
||||
typeof req.path !== 'string' ||
|
||||
!req.headers ||
|
||||
typeof req.headers !== 'object'
|
||||
) {
|
||||
throw new TypeError('all uiSettings methods must be passed a hapi.Request object');
|
||||
}
|
||||
}
|
||||
|
||||
export class UiSettings {
|
||||
constructor(server, status) {
|
||||
this._server = server;
|
||||
this._status = status;
|
||||
}
|
||||
|
||||
getDefaults() {
|
||||
return getDefaultSettings();
|
||||
}
|
||||
|
||||
// returns a Promise for the value of the requested setting
|
||||
async get(req, key) {
|
||||
assertRequest(req);
|
||||
return this.getAll(req)
|
||||
.then(all => all[key]);
|
||||
}
|
||||
|
||||
async getAll(req) {
|
||||
assertRequest(req);
|
||||
return this.getRaw(req)
|
||||
.then(raw => Object.keys(raw)
|
||||
.reduce((all, key) => {
|
||||
const item = raw[key];
|
||||
const hasUserValue = 'userValue' in item;
|
||||
all[key] = hasUserValue ? item.userValue : item.value;
|
||||
return all;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
|
||||
async getRaw(req) {
|
||||
assertRequest(req);
|
||||
return this.getUserProvided(req)
|
||||
.then(user => defaultsDeep(user, this.getDefaults()));
|
||||
}
|
||||
|
||||
async getUserProvided(req, { ignore401Errors = false } = {}) {
|
||||
assertRequest(req);
|
||||
const { callWithRequest, errors } = this._server.plugins.elasticsearch.getCluster('admin');
|
||||
|
||||
// If the ui settings status isn't green, we shouldn't be attempting to get
|
||||
// user settings, since we can't be sure that all the necessary conditions
|
||||
// (e.g. elasticsearch being available) are met.
|
||||
if (this._status.state !== 'green') {
|
||||
return hydrateUserSettings({});
|
||||
}
|
||||
|
||||
const params = this._getClientSettings();
|
||||
const allowedErrors = [errors[404], errors[403], errors.NoConnections];
|
||||
if (ignore401Errors) allowedErrors.push(errors[401]);
|
||||
|
||||
return Bluebird
|
||||
.resolve(callWithRequest(req, 'get', params, { wrap401Errors: !ignore401Errors }))
|
||||
.catch(...allowedErrors, () => ({}))
|
||||
.then(resp => resp._source || {})
|
||||
.then(source => hydrateUserSettings(source));
|
||||
}
|
||||
|
||||
async setMany(req, changes) {
|
||||
assertRequest(req);
|
||||
const { callWithRequest } = this._server.plugins.elasticsearch.getCluster('admin');
|
||||
const clientParams = {
|
||||
...this._getClientSettings(),
|
||||
body: { doc: changes }
|
||||
};
|
||||
return callWithRequest(req, 'update', clientParams)
|
||||
.then(() => ({}));
|
||||
}
|
||||
|
||||
async set(req, key, value) {
|
||||
assertRequest(req);
|
||||
return this.setMany(req, { [key]: value });
|
||||
}
|
||||
|
||||
async remove(req, key) {
|
||||
assertRequest(req);
|
||||
return this.set(req, key, null);
|
||||
}
|
||||
|
||||
async removeMany(req, keys) {
|
||||
assertRequest(req);
|
||||
const changes = {};
|
||||
keys.forEach(key => {
|
||||
changes[key] = null;
|
||||
});
|
||||
return this.setMany(req, changes);
|
||||
}
|
||||
|
||||
_getClientSettings() {
|
||||
const config = this._server.config();
|
||||
const index = config.get('kibana.index');
|
||||
const id = config.get('pkg.version');
|
||||
const type = 'config';
|
||||
return { index, type, id };
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { UiSettings } from './ui_settings';
|
||||
import { uiSettingsServiceFactory } from './ui_settings_service_factory';
|
||||
import { getUiSettingsServiceForRequest } from './ui_settings_service_for_request';
|
||||
import { mirrorStatus } from './mirror_status';
|
||||
|
||||
export function uiSettingsMixin(kbnServer, server, config) {
|
||||
|
@ -9,9 +10,35 @@ export function uiSettingsMixin(kbnServer, server, config) {
|
|||
return;
|
||||
}
|
||||
|
||||
const uiSettings = new UiSettings(server, status);
|
||||
server.decorate('server', 'uiSettings', () => uiSettings);
|
||||
// Passed to the UiSettingsService.
|
||||
// UiSettingsService calls the function before trying to read data from
|
||||
// elasticsearch, giving us a chance to prevent it from happening.
|
||||
//
|
||||
// If the ui settings status isn't green we shouldn't be attempting to get
|
||||
// user settings, since we can't be sure that all the necessary conditions
|
||||
// (e.g. elasticsearch being available) are met.
|
||||
const readUiSettingsInterceptor = () => {
|
||||
if (status.state !== 'green') {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
// don't return, just let it happen when the plugins are ready
|
||||
kbnServer.ready().then(() => {
|
||||
mirrorStatus(status, kbnServer.status.getForPluginId('elasticsearch'));
|
||||
});
|
||||
|
||||
server.decorate('server', 'uiSettingsServiceFactory', function (options) {
|
||||
return uiSettingsServiceFactory(server, options);
|
||||
});
|
||||
|
||||
server.decorate('request', 'getUiSettingsService', function () {
|
||||
return getUiSettingsServiceForRequest(server, this, readUiSettingsInterceptor);
|
||||
});
|
||||
|
||||
server.decorate('server', 'uiSettings', () => {
|
||||
throw new Error(`
|
||||
server.uiSettings has been removed, see https://github.com/elastic/kibana/pull/12243.
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
|
144
src/ui/ui_settings/ui_settings_service.js
Normal file
144
src/ui/ui_settings/ui_settings_service.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
import { defaultsDeep, noop } from 'lodash';
|
||||
import { errors as esErrors } from 'elasticsearch';
|
||||
|
||||
import { getDefaultSettings } from './defaults';
|
||||
|
||||
function hydrateUserSettings(userSettings) {
|
||||
return Object.keys(userSettings)
|
||||
.map(key => ({ key, userValue: userSettings[key] }))
|
||||
.filter(({ userValue }) => userValue !== null)
|
||||
.reduce((acc, { key, userValue }) => ({ ...acc, [key]: { userValue } }), {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Service that provides access to the UiSettings stored in elasticsearch.
|
||||
*
|
||||
* @class UiSettingsService
|
||||
* @param {Object} options
|
||||
* @property {string} options.index Elasticsearch index name where settings are stored
|
||||
* @property {string} options.type type of ui settings Elasticsearch doc
|
||||
* @property {string} options.id id of ui settings Elasticsearch doc
|
||||
* @property {AsyncFunction} options.callCluster function that accepts a method name and
|
||||
* param object which causes a request via some elasticsearch client
|
||||
* @property {AsyncFunction} [options.readInterceptor] async function that is called when the
|
||||
* UiSettingsService does a read() an has an oportunity to intercept the
|
||||
* request and return an alternate `_source` value to use.
|
||||
*/
|
||||
export class UiSettingsService {
|
||||
constructor(options) {
|
||||
const {
|
||||
index,
|
||||
type,
|
||||
id,
|
||||
callCluster,
|
||||
readInterceptor = noop,
|
||||
} = options;
|
||||
|
||||
this._callCluster = callCluster;
|
||||
this._readInterceptor = readInterceptor;
|
||||
this._index = index;
|
||||
this._type = type;
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
getDefaults() {
|
||||
return getDefaultSettings();
|
||||
}
|
||||
|
||||
// returns a Promise for the value of the requested setting
|
||||
async get(key) {
|
||||
const all = await this.getAll();
|
||||
return all[key];
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
const raw = await this.getRaw();
|
||||
|
||||
return Object.keys(raw)
|
||||
.reduce((all, key) => {
|
||||
const item = raw[key];
|
||||
const hasUserValue = 'userValue' in item;
|
||||
all[key] = hasUserValue ? item.userValue : item.value;
|
||||
return all;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async getRaw() {
|
||||
const userProvided = await this.getUserProvided();
|
||||
return defaultsDeep(userProvided, this.getDefaults());
|
||||
}
|
||||
|
||||
async getUserProvided(options) {
|
||||
return hydrateUserSettings(await this._read(options));
|
||||
}
|
||||
|
||||
async setMany(changes) {
|
||||
await this._write(changes);
|
||||
}
|
||||
|
||||
async set(key, value) {
|
||||
await this.setMany({ [key]: value });
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
await this.set(key, null);
|
||||
}
|
||||
|
||||
async removeMany(keys) {
|
||||
const changes = {};
|
||||
keys.forEach(key => {
|
||||
changes[key] = null;
|
||||
});
|
||||
await this.setMany(changes);
|
||||
}
|
||||
|
||||
async _write(changes) {
|
||||
await this._callCluster('update', {
|
||||
index: this._index,
|
||||
type: this._type,
|
||||
id: this._id,
|
||||
body: {
|
||||
doc: changes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _read(options = {}) {
|
||||
const interceptValue = await this._readInterceptor(options);
|
||||
if (interceptValue != null) {
|
||||
return interceptValue;
|
||||
}
|
||||
|
||||
const {
|
||||
ignore401Errors = false
|
||||
} = options;
|
||||
|
||||
const isIgnorableError = error => (
|
||||
error instanceof esErrors[404] ||
|
||||
error instanceof esErrors[403] ||
|
||||
error instanceof esErrors.NoConnections ||
|
||||
(ignore401Errors && error instanceof esErrors[401])
|
||||
);
|
||||
|
||||
try {
|
||||
const clientParams = {
|
||||
index: this._index,
|
||||
type: this._type,
|
||||
id: this._id,
|
||||
};
|
||||
|
||||
const callOptions = {
|
||||
wrap401Errors: !ignore401Errors
|
||||
};
|
||||
|
||||
const resp = await this._callCluster('get', clientParams, callOptions);
|
||||
return resp._source;
|
||||
} catch (error) {
|
||||
if (isIgnorableError(error)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
31
src/ui/ui_settings/ui_settings_service_factory.js
Normal file
31
src/ui/ui_settings/ui_settings_service_factory.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { UiSettingsService } from './ui_settings_service';
|
||||
|
||||
/**
|
||||
* Create an instance of UiSettingsService that will use the
|
||||
* passed `callCluster` function to communicate with elasticsearch
|
||||
*
|
||||
* @param {Hapi.Server} server
|
||||
* @param {Object} options
|
||||
* @property {AsyncFunction} options.callCluster function that accepts a method name and
|
||||
* param object which causes a request via some elasticsearch client
|
||||
* @property {AsyncFunction} [options.readInterceptor] async function that is called when the
|
||||
* UiSettingsService does a read() an has an oportunity to intercept the
|
||||
* request and return an alternate `_source` value to use.
|
||||
* @return {UiSettingsService}
|
||||
*/
|
||||
export function uiSettingsServiceFactory(server, options) {
|
||||
const config = server.config();
|
||||
|
||||
const {
|
||||
callCluster,
|
||||
readInterceptor
|
||||
} = options;
|
||||
|
||||
return new UiSettingsService({
|
||||
index: config.get('kibana.index'),
|
||||
type: 'config',
|
||||
id: config.get('pkg.version'),
|
||||
callCluster,
|
||||
readInterceptor,
|
||||
});
|
||||
}
|
20
src/ui/ui_settings/ui_settings_service_for_request.js
Normal file
20
src/ui/ui_settings/ui_settings_service_for_request.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { uiSettingsServiceFactory } from './ui_settings_service_factory';
|
||||
|
||||
const BY_REQUEST_CACHE = new WeakMap();
|
||||
|
||||
export function getUiSettingsServiceForRequest(server, request, readInterceptor) {
|
||||
if (BY_REQUEST_CACHE.has(request)) {
|
||||
return BY_REQUEST_CACHE.get(request);
|
||||
}
|
||||
|
||||
const adminCluster = server.plugins.elasticsearch.getCluster('admin');
|
||||
const uiSettingsService = uiSettingsServiceFactory(server, {
|
||||
readInterceptor,
|
||||
callCluster(...args) {
|
||||
return adminCluster.callWithRequest(request, ...args);
|
||||
}
|
||||
});
|
||||
|
||||
BY_REQUEST_CACHE.set(request, uiSettingsService);
|
||||
return uiSettingsService;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue