diff --git a/src/core_plugins/kibana/index.js b/src/core_plugins/kibana/index.js
index fbdd29dd8104c..1632d389a7a8f 100644
--- a/src/core_plugins/kibana/index.js
+++ b/src/core_plugins/kibana/index.js
@@ -12,6 +12,7 @@ import scripts from './server/routes/api/scripts';
import { registerSuggestionsApi } from './server/routes/api/suggestions';
import * as systemApi from './server/lib/system_api';
import mappings from './mappings.json';
+import { getUiSettingDefaults } from './ui_setting_defaults';
const mkdirp = Promise.promisify(mkdirpNode);
@@ -130,7 +131,9 @@ module.exports = function (kibana) {
translations: [
resolve(__dirname, './translations/en.json')
],
- mappings
+
+ mappings,
+ uiSettingDefaults: getUiSettingDefaults(),
},
preInit: async function (server) {
diff --git a/src/core_plugins/kibana/server/routes/api/settings/register_delete.js b/src/core_plugins/kibana/server/routes/api/settings/register_delete.js
index ea68620298d3d..1b78e83b5eac6 100644
--- a/src/core_plugins/kibana/server/routes/api/settings/register_delete.js
+++ b/src/core_plugins/kibana/server/routes/api/settings/register_delete.js
@@ -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)));
diff --git a/src/core_plugins/kibana/server/routes/api/settings/register_get.js b/src/core_plugins/kibana/server/routes/api/settings/register_get.js
index 85fc8c9ca5f27..123f869ef0672 100644
--- a/src/core_plugins/kibana/server/routes/api/settings/register_get.js
+++ b/src/core_plugins/kibana/server/routes/api/settings/register_get.js
@@ -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)));
}
diff --git a/src/core_plugins/kibana/server/routes/api/settings/register_set.js b/src/core_plugins/kibana/server/routes/api/settings/register_set.js
index 8d572bbd1ab25..f82cafef43324 100644
--- a/src/core_plugins/kibana/server/routes/api/settings/register_set.js
+++ b/src/core_plugins/kibana/server/routes/api/settings/register_set.js
@@ -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)));
diff --git a/src/core_plugins/kibana/server/routes/api/settings/register_set_many.js b/src/core_plugins/kibana/server/routes/api/settings/register_set_many.js
index 57ce8357fb1d2..71e873bd80d79 100644
--- a/src/core_plugins/kibana/server/routes/api/settings/register_set_many.js
+++ b/src/core_plugins/kibana/server/routes/api/settings/register_set_many.js
@@ -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)));
diff --git a/src/ui/ui_settings/defaults.js b/src/core_plugins/kibana/ui_setting_defaults.js
similarity index 89%
rename from src/ui/ui_settings/defaults.js
rename to src/core_plugins/kibana/ui_setting_defaults.js
index c7cc2555de98e..1c53ae111307d 100644
--- a/src/ui/ui_settings/defaults.js
+++ b/src/core_plugins/kibana/ui_setting_defaults.js
@@ -1,6 +1,6 @@
import moment from 'moment-timezone';
-export function getDefaultSettings() {
+export function getUiSettingDefaults() {
const weekdays = moment.weekdays().slice();
const [defaultWeekday] = weekdays;
@@ -283,47 +283,6 @@ export function getDefaultSettings() {
value: 2000,
description: 'The maximum number of buckets a single datasource can return'
},
- // Timelion stuff
- 'timelion:showTutorial': {
- value: false,
- description: 'Should I show the tutorial by default when entering the timelion app?'
- },
- 'timelion:es.timefield': {
- value: '@timestamp',
- description: 'Default field containing a timestamp when using .es()'
- },
- 'timelion:es.default_index': {
- value: '_all',
- description: 'Default elasticsearch index to search with .es()'
- },
- 'timelion:target_buckets': {
- value: 200,
- description: 'The number of buckets to shoot for when using auto intervals'
- },
- 'timelion:max_buckets': {
- value: 2000,
- description: 'The maximum number of buckets a single datasource can return'
- },
- 'timelion:default_columns': {
- value: 2,
- description: 'Number of columns on a timelion sheet by default'
- },
- 'timelion:default_rows': {
- value: 2,
- description: 'Number of rows on a timelion sheet by default'
- },
- 'timelion:min_interval': {
- value: '1ms',
- description: 'The smallest interval that will be calculated when using "auto"'
- },
- 'timelion:graphite.url': {
- value: 'https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite',
- description: '[experimental] The URL of your graphite host'
- },
- 'timelion:quandl.key': {
- value: 'someKeyHere',
- description: '[experimental] Your API key from www.quandl.com'
- },
'state:storeInSessionStorage': {
value: false,
description: 'The URL can sometimes grow to be too large for some browsers to ' +
diff --git a/src/core_plugins/tests_bundle/index.js b/src/core_plugins/tests_bundle/index.js
index e78f4bd008df9..335457290293e 100644
--- a/src/core_plugins/tests_bundle/index.js
+++ b/src/core_plugins/tests_bundle/index.js
@@ -1,5 +1,4 @@
import { union } from 'lodash';
-import { getDefaultSettings } from '../../ui/ui_settings/defaults';
import findSourceFiles from './find_source_files';
import { fromRoot } from '../../utils';
@@ -57,7 +56,11 @@ export default (kibana) => {
});
}
- env.defaultUiSettings = getDefaultSettings();
+ env.defaultUiSettings = plugins.kbnServer.uiExports.consumers
+ // find the first uiExportsConsumer that has a getUiSettingDefaults method
+ // See src/ui/ui_settings/ui_exports_consumer.js
+ .find(consumer => typeof consumer.getUiSettingDefaults === 'function')
+ .getUiSettingDefaults();
return new UiBundle({
id: 'tests',
diff --git a/src/core_plugins/timelion/index.js b/src/core_plugins/timelion/index.js
index 1da6f20a58a60..bf89fe194fa28 100644
--- a/src/core_plugins/timelion/index.js
+++ b/src/core_plugins/timelion/index.js
@@ -36,7 +36,50 @@ module.exports = function (kibana) {
visTypes: [
'plugins/timelion/vis'
],
- mappings: require('./mappings.json')
+ mappings: require('./mappings.json'),
+
+ uiSettingDefaults: {
+ 'timelion:showTutorial': {
+ value: false,
+ description: 'Should I show the tutorial by default when entering the timelion app?'
+ },
+ 'timelion:es.timefield': {
+ value: '@timestamp',
+ description: 'Default field containing a timestamp when using .es()'
+ },
+ 'timelion:es.default_index': {
+ value: '_all',
+ description: 'Default elasticsearch index to search with .es()'
+ },
+ 'timelion:target_buckets': {
+ value: 200,
+ description: 'The number of buckets to shoot for when using auto intervals'
+ },
+ 'timelion:max_buckets': {
+ value: 2000,
+ description: 'The maximum number of buckets a single datasource can return'
+ },
+ 'timelion:default_columns': {
+ value: 2,
+ description: 'Number of columns on a timelion sheet by default'
+ },
+ 'timelion:default_rows': {
+ value: 2,
+ description: 'Number of rows on a timelion sheet by default'
+ },
+ 'timelion:min_interval': {
+ value: '1ms',
+ description: 'The smallest interval that will be calculated when using "auto"'
+ },
+ 'timelion:graphite.url': {
+ value: 'https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite',
+ description: '[experimental] The URL of your graphite host'
+ },
+ 'timelion:quandl.key': {
+ value: 'someKeyHere',
+ description: '[experimental] Your API key from www.quandl.com'
+ }
+ }
},
init: require('./init.js'),
});
diff --git a/src/core_plugins/timelion/server/routes/run.js b/src/core_plugins/timelion/server/routes/run.js
index 6b5fee71f557a..3b4c349395cc8 100644
--- a/src/core_plugins/timelion/server/routes/run.js
+++ b/src/core_plugins/timelion/server/routes/run.js
@@ -14,7 +14,7 @@ module.exports = (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,
diff --git a/src/core_plugins/timelion/server/routes/validate_es.js b/src/core_plugins/timelion/server/routes/validate_es.js
index 6ade46b11dc25..5b61cbd4e7d92 100644
--- a/src/core_plugins/timelion/server/routes/validate_es.js
+++ b/src/core_plugins/timelion/server/routes/validate_es.js
@@ -3,8 +3,7 @@ module.exports = 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'];
diff --git a/src/functional_test_runner/cli.js b/src/functional_test_runner/cli.js
index 362176bc6fcba..a34a1511cf179 100644
--- a/src/functional_test_runner/cli.js
+++ b/src/functional_test_runner/cli.js
@@ -44,9 +44,9 @@ async function run() {
const failureCount = await functionalTestRunner.run();
process.exitCode = failureCount ? 1 : 0;
} catch (err) {
- // await teardown(err);
+ await teardown(err);
} finally {
- // await teardown();
+ await teardown();
}
}
diff --git a/src/server/http/index.js b/src/server/http/index.js
index 5564bb4ed6d50..e458f7183b07f 100644
--- a/src/server/http/index.js
+++ b/src/server/http/index.js
@@ -120,8 +120,8 @@ module.exports = 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;
diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js
index d35c9c1030737..51dd45bb9add7 100644
--- a/src/server/kbn_server.js
+++ b/src/server/kbn_server.js
@@ -16,7 +16,6 @@ import pluginsCheckEnabledMixin from './plugins/check_enabled';
import pluginsCheckVersionMixin from './plugins/check_version';
import configCompleteMixin from './config/complete';
import uiMixin from '../ui';
-import { uiSettingsMixin } from '../ui';
import optimizeMixin from '../optimize';
import pluginsInitializeMixin from './plugins/initialize';
import { indexPatternsMixin } from './index_patterns';
@@ -63,9 +62,6 @@ module.exports = class KbnServer {
// setup saved object routes
savedObjectsMixin,
- // setup server.uiSettings
- uiSettingsMixin,
-
// ensure that all bundles are built, or that the
// lazy bundle server is running
optimizeMixin,
diff --git a/src/ui/__tests__/ui_exports_replace_injected_vars.js b/src/ui/__tests__/ui_exports_replace_injected_vars.js
index 6ebd06df03237..662236464c90f 100644
--- a/src/ui/__tests__/ui_exports_replace_injected_vars.js
+++ b/src/ui/__tests__/ui_exports_replace_injected_vars.js
@@ -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 };
});
});
diff --git a/src/ui/index.js b/src/ui/index.js
index cec250e9d1b8e..97e176201cbac 100644
--- a/src/ui/index.js
+++ b/src/ui/index.js
@@ -10,13 +10,15 @@ import UiBundleCollection from './ui_bundle_collection';
import UiBundlerEnv from './ui_bundler_env';
import { UiI18n } from './ui_i18n';
-export { uiSettingsMixin } from './ui_settings';
+import { uiSettingsMixin } from './ui_settings';
export default async (kbnServer, server, config) => {
const uiExports = kbnServer.uiExports = new UiExports({
urlBasePath: config.get('server.basePath')
});
+ await kbnServer.mixin(uiSettingsMixin);
+
const uiI18n = kbnServer.uiI18n = new UiI18n(config.get('i18n.defaultLocale'));
uiI18n.addUiExportConsumer(uiExports);
@@ -67,7 +69,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 {
@@ -83,7 +85,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,
diff --git a/src/ui/ui_settings/__tests__/index.js b/src/ui/ui_settings/__tests__/index.js
deleted file mode 100644
index 40c681f2b9765..0000000000000
--- a/src/ui/ui_settings/__tests__/index.js
+++ /dev/null
@@ -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 };
-}
diff --git a/src/ui/ui_settings/__tests__/lib/call_cluster_stub.js b/src/ui/ui_settings/__tests__/lib/call_cluster_stub.js
new file mode 100644
index 0000000000000..568c18388912d
--- /dev/null
+++ b/src/ui/ui_settings/__tests__/lib/call_cluster_stub.js
@@ -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;
+}
diff --git a/src/ui/ui_settings/__tests__/lib/index.js b/src/ui/ui_settings/__tests__/lib/index.js
new file mode 100644
index 0000000000000..f3cd36fad2691
--- /dev/null
+++ b/src/ui/ui_settings/__tests__/lib/index.js
@@ -0,0 +1 @@
+export { createCallClusterStub } from './call_cluster_stub';
diff --git a/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js b/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js
new file mode 100644
index 0000000000000..a81d7d6d8d3a3
--- /dev/null
+++ b/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js
@@ -0,0 +1,206 @@
+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,
+ uiExports: { addConsumer: sinon.stub() },
+ 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);
+ decorations.server.uiSettingsServiceFactory({
+ foo: 'bar'
+ });
+ sinon.assert.calledOnce(uiSettingsServiceFactory);
+ sinon.assert.calledWithExactly(uiSettingsServiceFactory, server, {
+ foo: 'bar',
+ getDefaults: sinon.match.func,
+ });
+ });
+ });
+
+ 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 options = getUiSettingsServiceForRequest.firstCall.args[2];
+ expect(options).to.have.property('readInterceptor');
+
+ const { readInterceptor } = options;
+ 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');
+ });
+ });
+});
diff --git a/src/ui/ui_settings/__tests__/ui_settings_service.js b/src/ui/ui_settings/__tests__/ui_settings_service.js
new file mode 100644
index 0000000000000..ed121e004bc69
--- /dev/null
+++ b/src/ui/ui_settings/__tests__/ui_settings_service.js
@@ -0,0 +1,376 @@
+import { isEqual } from 'lodash';
+import expect from 'expect.js';
+import { errors as esErrors } from 'elasticsearch';
+import Chance from 'chance';
+
+import { UiSettingsService } from '../ui_settings_service';
+
+import { createCallClusterStub } from './lib';
+
+const INDEX = '.kibana';
+const TYPE = 'config';
+const ID = 'kibana-version';
+const chance = new Chance();
+
+function setup(options = {}) {
+ const {
+ readInterceptor,
+ getDefaults,
+ defaults = {},
+ esDocSource = {},
+ callCluster = createCallClusterStub(INDEX, TYPE, ID, esDocSource)
+ } = options;
+
+ const uiSettings = new UiSettingsService({
+ index: INDEX,
+ type: TYPE,
+ id: ID,
+ getDefaults: getDefaults || (() => defaults),
+ 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('returns a promise for the defaults', async () => {
+ const { uiSettings } = setup();
+ const promise = uiSettings.getDefaults();
+ expect(promise).to.be.a(Promise);
+ expect(await promise).to.eql({});
+ });
+ });
+
+ describe('getDefaults() argument', () => {
+ it('casts sync `getDefaults()` to promise', () => {
+ const getDefaults = () => ({ key: { value: chance.word() } });
+ const { uiSettings } = setup({ getDefaults });
+ expect(uiSettings.getDefaults()).to.be.a(Promise);
+ });
+
+ it('returns the defaults returned by getDefaults() argument', async () => {
+ const value = chance.word();
+ const { uiSettings } = setup({ defaults: { key: { value } } });
+ expect(await uiSettings.getDefaults()).to.eql({
+ key: { value }
+ });
+ });
+ });
+
+ 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 defaults = { key: { value: chance.word() } };
+ const { uiSettings } = setup({ esDocSource, defaults });
+ const result = await uiSettings.getRaw();
+ expect(result).to.eql(defaults);
+ });
+
+ it(`user configuration gets merged with defaults`, async () => {
+ const esDocSource = { foo: 'bar' };
+ const defaults = { key: { value: chance.word() } };
+ const { uiSettings } = setup({ esDocSource, defaults });
+ const result = await uiSettings.getRaw();
+
+ expect(result).to.eql({
+ foo: {
+ userValue: 'bar',
+ },
+ key: {
+ value: defaults.key.value,
+ },
+ });
+ });
+ });
+
+ describe('#getAll()', () => {
+ it('pulls user configuration from ES', async () => {
+ const esDocSource = {};
+ const { uiSettings, assertGetQuery } = setup({ esDocSource });
+ await uiSettings.getAll();
+ assertGetQuery();
+ });
+
+ it(`returns defaults when es doc is empty`, async () => {
+ const esDocSource = { };
+ const defaults = { foo: { value: 'bar' } };
+ const { uiSettings } = setup({ esDocSource, defaults });
+ expect(await uiSettings.getAll()).to.eql({
+ foo: 'bar'
+ });
+ });
+
+ it(`merges user values, including ones without defaults, into key value pairs`, async () => {
+ const esDocSource = {
+ foo: 'user-override',
+ bar: 'user-provided',
+ };
+
+ const defaults = {
+ foo: {
+ value: 'default'
+ },
+ };
+
+ const { uiSettings } = setup({ esDocSource, defaults });
+ expect(await uiSettings.getAll()).to.eql({
+ foo: 'user-override',
+ bar: 'user-provided',
+ });
+ });
+ });
+
+ 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 defaults = { dateFormat: { value: chance.word() } };
+ const { uiSettings } = setup({ esDocSource, defaults });
+ const result = await uiSettings.get('dateFormat');
+ 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({
+ defaults: {
+ foo: { value: 'foo' },
+ bar: { value: 'bar' },
+ },
+
+ readInterceptor: () => ({
+ foo: 'not foo'
+ }),
+ });
+
+ expect(await uiSettings.getAll()).to.eql({
+ foo: 'not foo',
+ bar: 'bar'
+ });
+ });
+ });
+ });
+});
diff --git a/src/ui/ui_settings/ui_exports_consumer.js b/src/ui/ui_settings/ui_exports_consumer.js
new file mode 100644
index 0000000000000..74962c8845049
--- /dev/null
+++ b/src/ui/ui_settings/ui_exports_consumer.js
@@ -0,0 +1,43 @@
+/**
+ * The UiExports class accepts consumer objects that it consults while
+ * trying to consume all of the `uiExport` declarations provided by
+ * plugins.
+ *
+ * UiExportConsumer is instantiated and passed to UiExports, then for
+ * every `uiExport` declaration the `exportConsumer(type)` method is
+ * with the key of the declaration. If this consumer knows how to handle
+ * that key we return a function that will be called with the plugins
+ * and values of all declarations using that key.
+ *
+ * With this, the consumer merges all of the declarations into the
+ * _uiSettingDefaults map, ensuring that there are not collisions along
+ * the way.
+ *
+ * @class UiExportsConsumer
+ */
+export class UiExportsConsumer {
+ _uiSettingDefaults = {};
+
+ exportConsumer(type) {
+ switch (type) {
+ case 'uiSettingDefaults':
+ return (plugin, settingDefinitions) => {
+ Object.keys(settingDefinitions).forEach((key) => {
+ if (key in this._uiSettingDefaults) {
+ throw new Error(`uiSettingDefaults for key "${key}" are already defined`);
+ }
+
+ this._uiSettingDefaults[key] = settingDefinitions[key];
+ });
+ };
+ }
+ }
+
+ /**
+ * Get the map of uiSettingNames to "default" specifications
+ * @return {Object}
+ */
+ getUiSettingDefaults() {
+ return this._uiSettingDefaults;
+ }
+}
diff --git a/src/ui/ui_settings/ui_settings.js b/src/ui/ui_settings/ui_settings.js
deleted file mode 100644
index e645cee68edc4..0000000000000
--- a/src/ui/ui_settings/ui_settings.js
+++ /dev/null
@@ -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 };
- }
-}
diff --git a/src/ui/ui_settings/ui_settings_mixin.js b/src/ui/ui_settings/ui_settings_mixin.js
index c8d37abbafc32..62b59db991542 100644
--- a/src/ui/ui_settings/ui_settings_mixin.js
+++ b/src/ui/ui_settings/ui_settings_mixin.js
@@ -1,17 +1,59 @@
-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';
+import { UiExportsConsumer } from './ui_exports_consumer';
export function uiSettingsMixin(kbnServer, server, config) {
const status = kbnServer.status.create('ui settings');
+ // reads the "uiSettingDefaults" from uiExports
+ const uiExportsConsumer = new UiExportsConsumer();
+ kbnServer.uiExports.addConsumer(uiExportsConsumer);
+
if (!config.get('uiSettings.enabled')) {
status.disabled('uiSettings.enabled config is set to `false`');
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 readInterceptor = () => {
+ if (status.state !== 'green') {
+ return {};
+ }
+ };
+
+ const getDefaults = () => (
+ uiExportsConsumer.getUiSettingDefaults()
+ );
+
+ // 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', (options = {}) => {
+ return uiSettingsServiceFactory(server, {
+ getDefaults,
+ ...options
+ });
+ });
+
+ server.decorate('request', 'getUiSettingsService', function () {
+ return getUiSettingsServiceForRequest(server, this, {
+ getDefaults,
+ readInterceptor,
+ });
+ });
+
+ server.decorate('server', 'uiSettings', () => {
+ throw new Error(`
+ server.uiSettings has been removed, see https://github.com/elastic/kibana/pull/12243.
+ `);
+ });
}
diff --git a/src/ui/ui_settings/ui_settings_service.js b/src/ui/ui_settings/ui_settings_service.js
new file mode 100644
index 0000000000000..57d0db8e9c96b
--- /dev/null
+++ b/src/ui/ui_settings/ui_settings_service.js
@@ -0,0 +1,146 @@
+import { defaultsDeep, noop } from 'lodash';
+import { errors as esErrors } from 'elasticsearch';
+
+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,
+ // we use a function for getDefaults() so that defaults can be different in
+ // different scenarios, and so they can change over time
+ getDefaults = () => ({}),
+ } = options;
+
+ this._callCluster = callCluster;
+ this._getDefaults = getDefaults;
+ this._readInterceptor = readInterceptor;
+ this._index = index;
+ this._type = type;
+ this._id = id;
+ }
+
+ async getDefaults() {
+ return await this._getDefaults();
+ }
+
+ // 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, await 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;
+ }
+ }
+}
diff --git a/src/ui/ui_settings/ui_settings_service_factory.js b/src/ui/ui_settings/ui_settings_service_factory.js
new file mode 100644
index 0000000000000..c0b70223a3dda
--- /dev/null
+++ b/src/ui/ui_settings/ui_settings_service_factory.js
@@ -0,0 +1,35 @@
+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.getDefaults] async function that returns defaults/details about
+ * the uiSettings.
+ * @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,
+ getDefaults,
+ } = options;
+
+ return new UiSettingsService({
+ index: config.get('kibana.index'),
+ type: 'config',
+ id: config.get('pkg.version'),
+ callCluster,
+ readInterceptor,
+ getDefaults,
+ });
+}
diff --git a/src/ui/ui_settings/ui_settings_service_for_request.js b/src/ui/ui_settings/ui_settings_service_for_request.js
new file mode 100644
index 0000000000000..d43657b6d8f42
--- /dev/null
+++ b/src/ui/ui_settings/ui_settings_service_for_request.js
@@ -0,0 +1,42 @@
+import { uiSettingsServiceFactory } from './ui_settings_service_factory';
+
+const BY_REQUEST_CACHE = new WeakMap();
+
+/**
+ * Get/create an instance of UiSettingsService bound to a specific request.
+ * Each call is cached (keyed on the request object itself) and subsequent
+ * requests will get the first UiSettingsService instance even if the `options`
+ * have changed.
+ *
+ * @param {Hapi.Server} server
+ * @param {Hapi.Request} request
+ * @param {Object} [options={}]
+ * @property {AsyncFunction} [options.getDefaults] async function that returns defaults/details about
+ * the uiSettings.
+ * @property {AsyncFunction} [options.readInterceptor] async function that is called when the
+ * UiSettingsService does a read() and has an oportunity to intercept the
+ * request and return an alternate `_source` value to use.
+ * @return {UiSettingsService}
+ */
+export function getUiSettingsServiceForRequest(server, request, options = {}) {
+ if (BY_REQUEST_CACHE.has(request)) {
+ return BY_REQUEST_CACHE.get(request);
+ }
+
+ const {
+ readInterceptor,
+ getDefaults
+ } = options;
+
+ const adminCluster = server.plugins.elasticsearch.getCluster('admin');
+ const uiSettingsService = uiSettingsServiceFactory(server, {
+ readInterceptor,
+ getDefaults,
+ callCluster(...args) {
+ return adminCluster.callWithRequest(request, ...args);
+ }
+ });
+
+ BY_REQUEST_CACHE.set(request, uiSettingsService);
+ return uiSettingsService;
+}