From 027a1c4fa9a1aab126c9bdbd2838b42f24a9bc1e Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 1 Nov 2017 16:18:18 -0700 Subject: [PATCH 01/67] [plugins] extract plugin discover from the kibana server --- src/core_plugins/dev_mode/index.js | 10 +- .../__tests__/collect_ui_exports.js | 55 ++++++ .../__tests__/find_plugin_specs.js | 72 ++++++++ src/plugin_discovery/collect_ui_exports.js | 11 ++ src/plugin_discovery/errors.js | 33 ++++ src/plugin_discovery/find_plugin_specs.js | 105 +++++++++++ src/plugin_discovery/index.js | 2 + .../plugin_config/extend_config_service.js | 31 ++++ src/plugin_discovery/plugin_config/index.js | 4 + src/plugin_discovery/plugin_config/schema.js | 23 +++ .../plugin_config/settings.js | 25 +++ .../__tests__/reduce_export_specs.js | 77 ++++++++ src/plugin_discovery/plugin_exports/index.js | 1 + .../plugin_exports/reduce_export_specs.js | 28 +++ src/plugin_discovery/plugin_pack/index.js | 3 + src/plugin_discovery/plugin_pack/lib/fs.js | 56 ++++++ src/plugin_discovery/plugin_pack/lib/index.js | 5 + .../plugin_pack/pack_at_path.js | 42 +++++ .../plugin_pack/packs_in_directory.js | 30 ++++ .../plugin_pack/plugin_pack.js | 54 ++++++ src/plugin_discovery/plugin_spec/index.js | 1 + .../plugin_spec/plugin_spec.js | 167 ++++++++++++++++++ src/plugin_discovery/ui_export_defaults.js | 41 +++++ src/plugin_discovery/ui_export_types/index.js | 55 ++++++ .../ui_export_types/modify_injected_vars.js | 13 ++ .../ui_export_types/modify_reduce/alias.js | 10 ++ .../ui_export_types/modify_reduce/debug.js | 12 ++ .../ui_export_types/modify_reduce/index.js | 5 + .../ui_export_types/modify_reduce/map_spec.js | 11 ++ .../modify_reduce/unique_keys.js | 10 ++ .../ui_export_types/modify_reduce/wrap.js | 9 + .../ui_export_types/reduce/concat.js | 11 ++ .../ui_export_types/reduce/concat_values.js | 15 ++ .../ui_export_types/reduce/index.js | 3 + .../ui_export_types/reduce/merge.js | 6 + .../ui_export_types/reduce/merge_type.js | 10 ++ .../ui_export_types/reduce/unique_assign.js | 12 ++ .../ui_export_types/saved_object_mappings.js | 5 + .../ui_export_types/ui_apps.js | 47 +++++ .../ui_export_types/ui_extensions.js | 38 ++++ .../ui_export_types/ui_i18n.js | 5 + .../ui_export_types/ui_nav_links.js | 5 + .../ui_export_types/ui_settings.js | 4 + .../ui_export_types/unknown.js | 4 + .../ui_export_types/webpack_customizations.js | 5 + src/plugin_discovery/utils/combine_latest.js | 5 + src/plugin_discovery/utils/concat.js | 5 + src/plugin_discovery/utils/create.js | 5 + src/plugin_discovery/utils/debug.js | 13 ++ src/plugin_discovery/utils/defer.js | 5 + src/plugin_discovery/utils/empty.js | 3 + src/plugin_discovery/utils/fcb.js | 14 ++ src/plugin_discovery/utils/from.js | 5 + src/plugin_discovery/utils/from_event.js | 5 + src/plugin_discovery/utils/from_promise.js | 5 + src/plugin_discovery/utils/index.js | 14 ++ src/plugin_discovery/utils/merge.js | 5 + src/plugin_discovery/utils/of.js | 5 + src/plugin_discovery/utils/race.js | 5 + src/plugin_discovery/utils/throw.js | 5 + src/plugin_discovery/utils/timer.js | 5 + src/server/config/__tests__/config.js | 2 +- src/server/config/config.js | 2 +- src/server/config/index.js | 2 + src/server/config/setup.js | 2 +- 65 files changed, 1279 insertions(+), 4 deletions(-) create mode 100644 src/plugin_discovery/__tests__/collect_ui_exports.js create mode 100644 src/plugin_discovery/__tests__/find_plugin_specs.js create mode 100644 src/plugin_discovery/collect_ui_exports.js create mode 100644 src/plugin_discovery/errors.js create mode 100644 src/plugin_discovery/find_plugin_specs.js create mode 100644 src/plugin_discovery/index.js create mode 100644 src/plugin_discovery/plugin_config/extend_config_service.js create mode 100644 src/plugin_discovery/plugin_config/index.js create mode 100644 src/plugin_discovery/plugin_config/schema.js create mode 100644 src/plugin_discovery/plugin_config/settings.js create mode 100644 src/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js create mode 100644 src/plugin_discovery/plugin_exports/index.js create mode 100644 src/plugin_discovery/plugin_exports/reduce_export_specs.js create mode 100644 src/plugin_discovery/plugin_pack/index.js create mode 100644 src/plugin_discovery/plugin_pack/lib/fs.js create mode 100644 src/plugin_discovery/plugin_pack/lib/index.js create mode 100644 src/plugin_discovery/plugin_pack/pack_at_path.js create mode 100644 src/plugin_discovery/plugin_pack/packs_in_directory.js create mode 100644 src/plugin_discovery/plugin_pack/plugin_pack.js create mode 100644 src/plugin_discovery/plugin_spec/index.js create mode 100644 src/plugin_discovery/plugin_spec/plugin_spec.js create mode 100644 src/plugin_discovery/ui_export_defaults.js create mode 100644 src/plugin_discovery/ui_export_types/index.js create mode 100644 src/plugin_discovery/ui_export_types/modify_injected_vars.js create mode 100644 src/plugin_discovery/ui_export_types/modify_reduce/alias.js create mode 100644 src/plugin_discovery/ui_export_types/modify_reduce/debug.js create mode 100644 src/plugin_discovery/ui_export_types/modify_reduce/index.js create mode 100644 src/plugin_discovery/ui_export_types/modify_reduce/map_spec.js create mode 100644 src/plugin_discovery/ui_export_types/modify_reduce/unique_keys.js create mode 100644 src/plugin_discovery/ui_export_types/modify_reduce/wrap.js create mode 100644 src/plugin_discovery/ui_export_types/reduce/concat.js create mode 100644 src/plugin_discovery/ui_export_types/reduce/concat_values.js create mode 100644 src/plugin_discovery/ui_export_types/reduce/index.js create mode 100644 src/plugin_discovery/ui_export_types/reduce/merge.js create mode 100644 src/plugin_discovery/ui_export_types/reduce/merge_type.js create mode 100644 src/plugin_discovery/ui_export_types/reduce/unique_assign.js create mode 100644 src/plugin_discovery/ui_export_types/saved_object_mappings.js create mode 100644 src/plugin_discovery/ui_export_types/ui_apps.js create mode 100644 src/plugin_discovery/ui_export_types/ui_extensions.js create mode 100644 src/plugin_discovery/ui_export_types/ui_i18n.js create mode 100644 src/plugin_discovery/ui_export_types/ui_nav_links.js create mode 100644 src/plugin_discovery/ui_export_types/ui_settings.js create mode 100644 src/plugin_discovery/ui_export_types/unknown.js create mode 100644 src/plugin_discovery/ui_export_types/webpack_customizations.js create mode 100644 src/plugin_discovery/utils/combine_latest.js create mode 100644 src/plugin_discovery/utils/concat.js create mode 100644 src/plugin_discovery/utils/create.js create mode 100644 src/plugin_discovery/utils/debug.js create mode 100644 src/plugin_discovery/utils/defer.js create mode 100644 src/plugin_discovery/utils/empty.js create mode 100644 src/plugin_discovery/utils/fcb.js create mode 100644 src/plugin_discovery/utils/from.js create mode 100644 src/plugin_discovery/utils/from_event.js create mode 100644 src/plugin_discovery/utils/from_promise.js create mode 100644 src/plugin_discovery/utils/index.js create mode 100644 src/plugin_discovery/utils/merge.js create mode 100644 src/plugin_discovery/utils/of.js create mode 100644 src/plugin_discovery/utils/race.js create mode 100644 src/plugin_discovery/utils/throw.js create mode 100644 src/plugin_discovery/utils/timer.js create mode 100644 src/server/config/index.js diff --git a/src/core_plugins/dev_mode/index.js b/src/core_plugins/dev_mode/index.js index f06eeb323f2465..b44a3c52d204ca 100644 --- a/src/core_plugins/dev_mode/index.js +++ b/src/core_plugins/dev_mode/index.js @@ -1,6 +1,14 @@ export default (kibana) => { - if (!kibana.config.get('env.dev')) return; return new kibana.Plugin({ + id: 'dev_mode', + + isEnabled(config) { + return ( + config.get('env.dev') && + config.get('dev_mode.enabled') + ); + }, + uiExports: { spyModes: [ 'plugins/dev_mode/vis_debug_spy_panel' diff --git a/src/plugin_discovery/__tests__/collect_ui_exports.js b/src/plugin_discovery/__tests__/collect_ui_exports.js new file mode 100644 index 00000000000000..ab5781078baca3 --- /dev/null +++ b/src/plugin_discovery/__tests__/collect_ui_exports.js @@ -0,0 +1,55 @@ +import expect from 'expect.js'; + +import { collectUiExports } from '../collect_ui_exports'; +import { PluginPack } from '../plugin_pack'; + +const specs = new PluginPack({ + path: '/dev/null', + pkg: { + name: 'test', + version: 'kibana' + }, + provider({ Plugin }) { + return [ + new Plugin({ + id: 'test', + uiExports: { + visTypes: [ + 'plugin/test/visType1', + 'plugin/test/visType2', + 'plugin/test/visType3', + ] + } + }), + new Plugin({ + id: 'test2', + uiExports: { + visTypes: [ + 'plugin/test2/visType1', + 'plugin/test2/visType2', + 'plugin/test2/visType3', + ] + } + }), + ]; + } +}).getPluginSpecs(); + +describe('plugin discovery', () => { + describe('collectUiExports()', () => { + it('merges uiExports from all provided plugin specs', () => { + const uiExports = collectUiExports(specs); + const exported = uiExports.appExtensions.visTypes + .sort((a, b) => a.localeCompare(b)); + + expect(exported).to.eql([ + 'plugin/test/visType1', + 'plugin/test/visType2', + 'plugin/test/visType3', + 'plugin/test2/visType1', + 'plugin/test2/visType2', + 'plugin/test2/visType3' + ]); + }); + }); +}); diff --git a/src/plugin_discovery/__tests__/find_plugin_specs.js b/src/plugin_discovery/__tests__/find_plugin_specs.js new file mode 100644 index 00000000000000..be3485ee0ac0d5 --- /dev/null +++ b/src/plugin_discovery/__tests__/find_plugin_specs.js @@ -0,0 +1,72 @@ +import { resolve } from 'path'; +import { readdirSync } from 'fs'; + +import expect from 'expect.js'; +import { findPluginSpecs } from '../find_plugin_specs'; +import { PluginSpec } from '../plugin_spec'; + +const CORE_PLUGINS = resolve(__dirname, '../../core_plugins'); + +describe('plugin discovery', () => { + describe('findPluginSpecs()', function () { + this.timeout(10000); + + it('finds specs for specified plugin paths', async () => { + const { spec$ } = findPluginSpecs({ + plugins: { + paths: [ + resolve(CORE_PLUGINS, 'console'), + resolve(CORE_PLUGINS, 'elasticsearch'), + ] + } + }); + + const specs = await spec$.toArray().toPromise(); + expect(specs).to.have.length(2); + expect(specs[0]).to.be.a(PluginSpec); + expect(specs[0].getId()).to.be('console'); + expect(specs[1].getId()).to.be('elasticsearch'); + }); + + it('finds all specs in scanDirs', async () => { + const { spec$ } = findPluginSpecs({ + // used to ensure the dev_mode plugin is enabled + env: 'development', + + plugins: { + scanDirs: [CORE_PLUGINS] + } + }); + + const expected = readdirSync(CORE_PLUGINS) + .filter(name => !name.startsWith('.')) + .sort((a, b) => a.localeCompare(b)); + + const specs = await spec$.toArray().toPromise(); + const specIds = specs + .map(spec => spec.getId()) + .sort((a, b) => a.localeCompare(b)); + + expect(specIds).to.eql(expected); + }); + + it('does not find disabled plugins', async () => { + const { spec$ } = findPluginSpecs({ + elasticsearch: { + enabled: false + }, + + plugins: { + paths: [ + resolve(CORE_PLUGINS, 'elasticsearch'), + resolve(CORE_PLUGINS, 'kibana') + ] + } + }); + + const specs = await spec$.toArray().toPromise(); + expect(specs).to.have.length(1); + expect(specs[0].getId()).to.be('kibana'); + }); + }); +}); diff --git a/src/plugin_discovery/collect_ui_exports.js b/src/plugin_discovery/collect_ui_exports.js new file mode 100644 index 00000000000000..4e9e981631c273 --- /dev/null +++ b/src/plugin_discovery/collect_ui_exports.js @@ -0,0 +1,11 @@ +import { UI_EXPORT_DEFAULTS } from './ui_export_defaults'; +import * as uiExportTypeReducers from './ui_export_types'; +import { reduceExportSpecs } from './plugin_exports'; + +export function collectUiExports(pluginSpecs) { + return reduceExportSpecs( + pluginSpecs, + uiExportTypeReducers, + UI_EXPORT_DEFAULTS + ); +} diff --git a/src/plugin_discovery/errors.js b/src/plugin_discovery/errors.js new file mode 100644 index 00000000000000..7553b7ef86b07d --- /dev/null +++ b/src/plugin_discovery/errors.js @@ -0,0 +1,33 @@ + +const $code = Symbol('FindErrorCode'); + +/** + * Thrown when reading a plugin directory fails, wraps failure + * @type {String} + */ +const ERROR_INVALID_DIRECTORY = 'ERROR_INVALID_DIRECTORY'; +export function createInvalidDirectoryError(sourceError, path) { + sourceError[$code] = ERROR_INVALID_DIRECTORY; + sourceError.path = path; + return sourceError; +} +export function isInvalidDirectoryError(error) { + return error && error[$code] === ERROR_INVALID_DIRECTORY; +} + + +/** + * Thrown when trying to create a PluginPack for a path that + * is not a valid plugin definition + * @type {String} + */ +const ERROR_INVALID_PACK = 'ERROR_INVALID_PACK'; +export function createInvalidPackError(path, reason) { + const error = new Error(`PluginPack${path ? ` at "${path}"` : ''} ${reason}`); + error[$code] = ERROR_INVALID_PACK; + error.path = path; + return error; +} +export function isInvalidPackError(error) { + return error && error[$code] === ERROR_INVALID_PACK; +} diff --git a/src/plugin_discovery/find_plugin_specs.js b/src/plugin_discovery/find_plugin_specs.js new file mode 100644 index 00000000000000..0baf7f2a1d34c9 --- /dev/null +++ b/src/plugin_discovery/find_plugin_specs.js @@ -0,0 +1,105 @@ +import { $merge } from './utils'; + +import { transformDeprecations, Config } from '../server/config'; + +import { + extendConfigService, + disableConfigExtension, +} from './plugin_config'; + +import { + createPackAtPath$, + createPacksInDirectory$, +} from './plugin_pack'; + +import { + isInvalidDirectoryError, + isInvalidPackError, +} from './errors'; + +function defaultConfig(settings) { + return Config.withDefaultSchema( + transformDeprecations(settings) + ); +} + +/** + * Creates a collection of observables for discovering pluginSpecs + * using Kibana's defaults, settings, and config service + * + * @param {Object} settings + * @param {ConfigService} [config] when supplied **it is mutated** to include + * the config from discovered plugin specs + * @return {Object} + */ +export function findPluginSpecs(settings, config = defaultConfig(settings)) { + // find plugin packs in configured paths/dirs + const find$ = $merge( + ...config.get('plugins.paths').map(createPackAtPath$), + ...config.get('plugins.scanDirs').map(createPacksInDirectory$) + ) + .share(); + + const extendConfig$ = find$ + // get the specs for each found plugin pack + .mergeMap(({ pack }) => ( + pack ? pack.getPluginSpecs() : [] + )) + .mergeMap(async (spec) => { + // extend the config service with this plugin spec and + // collect its deprecations messages if some of its + // settings are outdated + const deprecations = []; + await extendConfigService(spec, config, settings, (message) => { + deprecations.push({ spec, message }); + }); + + // if the pluginSpec is disabled then disable the extension + // we made to the config service + const enabled = spec.isEnabled(config); + if (!enabled) { + disableConfigExtension(spec, config); + } + + return { + deprecations, + enabledSpecs: enabled ? [spec] : [], + }; + }) + .share(); + + return { + // plugin packs found when searching configured paths + pack$: find$ + .mergeMap(result => ( + result.pack ? [result.pack] : [] + )), + + // errors caused by invalid directories of plugin directories + invalidDirectoryError$: find$ + .mergeMap(result => ( + isInvalidDirectoryError(result.error) ? [result.error] : [] + )), + + // errors caused by directories that we expected to be plugin but were invalid + invalidPackError$: find$ + .mergeMap(result => ( + isInvalidPackError(result.error) ? [result.error] : [] + )), + + // { spec, message } objects produces when transforming deprecated + // settings for a plugin spec + deprecation$: extendConfig$ + .mergeMap(result => result.deprecations), + + // the config service we extended with all of the plugin specs, + // only emitted once it is fully extended by all + extendedConfig$: extendConfig$ + .ignoreElements() + .concat([config]), + + // all enabled PluginSpec objects + spec$: extendConfig$ + .mergeMap(result => result.enabledSpecs), + }; +} diff --git a/src/plugin_discovery/index.js b/src/plugin_discovery/index.js new file mode 100644 index 00000000000000..bc5b4a1d74f276 --- /dev/null +++ b/src/plugin_discovery/index.js @@ -0,0 +1,2 @@ +export { findPluginSpecs } from './find_plugin_specs'; +export { collectUiExports } from './collect_ui_exports'; diff --git a/src/plugin_discovery/plugin_config/extend_config_service.js b/src/plugin_discovery/plugin_config/extend_config_service.js new file mode 100644 index 00000000000000..fdd33993da1ba9 --- /dev/null +++ b/src/plugin_discovery/plugin_config/extend_config_service.js @@ -0,0 +1,31 @@ +import { getSettings } from './settings'; +import { getSchema, getStubSchema } from './schema'; + +/** + * Extend a config service with the schema and settings for a + * plugin spec and optionally call logDeprecation with warning + * messages about deprecated settings that are used + * @param {PluginSpec} spec + * @param {Server.Config} config + * @param {Object} rootSettings + * @param {Function} [logDeprecation] + * @return {Promise} + */ +export async function extendConfigService(spec, config, rootSettings, logDeprecation) { + const settings = await getSettings(spec, rootSettings, logDeprecation); + const schema = await getSchema(spec); + config.extendSchema(schema, settings, spec.getConfigPrefix()); +} + +/** + * Disable the schema and settings applied to a config service for + * a plugin spec + * @param {PluginSpec} spec + * @param {Server.Config} config + * @return {undefined} + */ +export function disableConfigExtension(spec, config) { + const prefix = spec.getConfigPrefix(); + config.removeSchema(prefix); + config.extendSchema(getStubSchema(), { enabled: false }, prefix); +} diff --git a/src/plugin_discovery/plugin_config/index.js b/src/plugin_discovery/plugin_config/index.js new file mode 100644 index 00000000000000..3d1a084b99fe52 --- /dev/null +++ b/src/plugin_discovery/plugin_config/index.js @@ -0,0 +1,4 @@ +export { + extendConfigService, + disableConfigExtension, +} from './extend_config_service'; diff --git a/src/plugin_discovery/plugin_config/schema.js b/src/plugin_discovery/plugin_config/schema.js new file mode 100644 index 00000000000000..0a4c690fef5ac6 --- /dev/null +++ b/src/plugin_discovery/plugin_config/schema.js @@ -0,0 +1,23 @@ +import Joi from 'joi'; + +const STUB_CONFIG_SCHEMA = Joi.object().keys({ + enabled: Joi.valid(false) +}); + +const DEFAULT_CONFIG_SCHEMA = Joi.object().keys({ + enabled: Joi.boolean().default(true) +}).default(); + + +/** + * Get the config schema for a plugin spec + * @param {PluginSpec} spec + * @return {Promise} + */ +export async function getSchema(spec) { + return await spec.getConfigSchemaProvider()(Joi) || DEFAULT_CONFIG_SCHEMA; +} + +export function getStubSchema() { + return STUB_CONFIG_SCHEMA; +} diff --git a/src/plugin_discovery/plugin_config/settings.js b/src/plugin_discovery/plugin_config/settings.js new file mode 100644 index 00000000000000..f052e9052d4002 --- /dev/null +++ b/src/plugin_discovery/plugin_config/settings.js @@ -0,0 +1,25 @@ +import { get } from 'lodash'; + +import * as serverConfig from '../../server/config'; +import { createTransform, Deprecations } from '../../deprecation'; + +async function getDeprecationTransformer(spec) { + const provider = spec.getDeprecationsProvider(); + return createTransform(await provider(Deprecations) || []); +} + +/** + * Get the settings for a pluginSpec from the raw root settings while + * optionally calling logDeprecation() with warnings about deprecated + * settings that were used + * @param {PluginSpec} spec + * @param {Object} rootSettings + * @param {Function} [logDeprecation] + * @return {Promise} + */ +export async function getSettings(spec, rootSettings, logDeprecation) { + const prefix = spec.getConfigPrefix(); + const transformer = await getDeprecationTransformer(spec); + const rawSettings = get(serverConfig.transformDeprecations(rootSettings), prefix); + return transformer(rawSettings, logDeprecation); +} diff --git a/src/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js b/src/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js new file mode 100644 index 00000000000000..4b05b79ada9e12 --- /dev/null +++ b/src/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js @@ -0,0 +1,77 @@ +import expect from 'expect.js'; + +import { PluginPack } from '../../plugin_pack'; +import { reduceExportSpecs } from '../reduce_export_specs'; + +const PLUGIN = new PluginPack({ + path: __dirname, + pkg: { + name: 'foo', + version: 'kibana' + }, + provider: ({ Plugin }) => ( + new Plugin({ + uiExports: { + concatNames: { + name: 'export1' + }, + + concat: [ + 'export2', + 'export3', + ], + } + }) + ) +}); + +const REDUCERS = { + concatNames(acc, spec, type, pluginSpec) { + return { + names: [].concat( + acc.names || [], + `${pluginSpec.getId()}:${spec.name}`, + ) + }; + }, + concat(acc, spec, type, pluginSpec) { + return { + names: [].concat( + acc.names || [], + `${pluginSpec.getId()}:${spec}`, + ) + }; + }, +}; + +const PLUGIN_SPECS = PLUGIN.getPluginSpecs(); + +describe('reduceExportSpecs', () => { + it('combines ui exports from a list of plugin definitions', () => { + const exports = reduceExportSpecs(PLUGIN_SPECS, REDUCERS); + expect(exports).to.eql({ + names: [ + 'foo:export1', + 'foo:export2', + 'foo:export3', + ] + }); + }); + + it('starts with the defaults', () => { + const exports = reduceExportSpecs(PLUGIN_SPECS, REDUCERS, { + names: [ + 'default' + ] + }); + + expect(exports).to.eql({ + names: [ + 'default', + 'foo:export1', + 'foo:export2', + 'foo:export3', + ] + }); + }); +}); diff --git a/src/plugin_discovery/plugin_exports/index.js b/src/plugin_discovery/plugin_exports/index.js new file mode 100644 index 00000000000000..3e72163b30709c --- /dev/null +++ b/src/plugin_discovery/plugin_exports/index.js @@ -0,0 +1 @@ +export { reduceExportSpecs } from './reduce_export_specs'; diff --git a/src/plugin_discovery/plugin_exports/reduce_export_specs.js b/src/plugin_discovery/plugin_exports/reduce_export_specs.js new file mode 100644 index 00000000000000..fb214339fea50f --- /dev/null +++ b/src/plugin_discovery/plugin_exports/reduce_export_specs.js @@ -0,0 +1,28 @@ +/** + * Combine the exportSpecs from a list of pluginSpecs + * by calling the reducers for each export type + * @param {Array} pluginSpecs + * @param {Object} exportTypes + * @param {Object} + */ +export function reduceExportSpecs(pluginSpecs, reducers, defaults = {}) { + return pluginSpecs.reduce((acc, pluginSpec) => { + const specsByType = pluginSpec.getExportSpecs(); + const types = Object.keys(specsByType); + + return types.reduce((acc, type) => { + const reducer = (reducers[type] || reducers.unknown); + + if (!reducer) { + throw new Error(`Unknown export type ${type}`); + } + + const specs = [].concat(specsByType[type] || []); + + return specs.reduce((acc, spec) => ( + reducer(acc, spec, type, pluginSpec) + ), acc); + }, acc); + }, defaults); +} diff --git a/src/plugin_discovery/plugin_pack/index.js b/src/plugin_discovery/plugin_pack/index.js new file mode 100644 index 00000000000000..93c552520088e7 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/index.js @@ -0,0 +1,3 @@ +export { PluginPack } from './plugin_pack'; +export { createPackAtPath$ } from './pack_at_path'; +export { createPacksInDirectory$ } from './packs_in_directory'; diff --git a/src/plugin_discovery/plugin_pack/lib/fs.js b/src/plugin_discovery/plugin_pack/lib/fs.js new file mode 100644 index 00000000000000..fb21b910a96b6d --- /dev/null +++ b/src/plugin_discovery/plugin_pack/lib/fs.js @@ -0,0 +1,56 @@ +import { stat, readdir } from 'fs'; +import { resolve } from 'path'; + +import { fromNode as fcb } from 'bluebird'; + +import { $fcb, $fromPromise } from '../../utils'; +import { createInvalidDirectoryError } from '../../errors'; + +async function statTest(path, test) { + try { + const stats = await fcb(cb => stat(path, cb)); + return Boolean(test(stats)); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + return false; +} + +/** + * Determine if a path currently points to a file + * @param {String} path + * @return {Promise} + */ +export async function isFile(path) { + return await statTest(path, stat => stat.isFile()); +} + +/** + * Determine if a path currently points to a directory + * @param {String} path + * @return {Promise} + */ +export async function isDirectory(path) { + return await statTest(path, stat => stat.isDirectory()); +} + +/** + * Get absolute paths for child directories within a path + * @param {string} path + * @return {Promise>} + */ +export const createChildDirectory$ = (path) => ( + $fcb(cb => readdir(path, cb)) + .catch(error => { + throw createInvalidDirectoryError(error, path); + }) + .mergeAll() + .filter(name => !name.startsWith('.')) + .map(name => resolve(path, name)) + .mergeMap(v => ( + $fromPromise(isDirectory(path)) + .mergeMap(pass => pass ? [v] : []) + )) +); diff --git a/src/plugin_discovery/plugin_pack/lib/index.js b/src/plugin_discovery/plugin_pack/lib/index.js new file mode 100644 index 00000000000000..9bfa5045bfa428 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/lib/index.js @@ -0,0 +1,5 @@ +export { + isFile, + isDirectory, + createChildDirectory$, +} from './fs'; diff --git a/src/plugin_discovery/plugin_pack/pack_at_path.js b/src/plugin_discovery/plugin_pack/pack_at_path.js new file mode 100644 index 00000000000000..3767577fe24a4c --- /dev/null +++ b/src/plugin_discovery/plugin_pack/pack_at_path.js @@ -0,0 +1,42 @@ +import { $from } from '../utils'; +import { isAbsolute, resolve } from 'path'; +import { createInvalidPackError } from '../errors'; + +import { isDirectory, isFile } from './lib'; +import { PluginPack } from './plugin_pack'; + +async function createPackAtPath(path) { + if (!isAbsolute(path)) { + throw createInvalidPackError(null, 'requires an absolute path'); + } + + if (!await isDirectory(path)) { + throw createInvalidPackError(path, 'must be a directory'); + } + + const pkgPath = resolve(path, 'package.json'); + if (!await isFile(pkgPath)) { + throw createInvalidPackError(path, 'must have a package.json file'); + } + + const pkg = require(pkgPath); + if (!pkg || typeof pkg !== 'object') { + throw createInvalidPackError(path, 'must have a value package.json file'); + } + + let provider = require(path); + if (provider.__esModule) { + provider = provider.default; + } + if (typeof provider !== 'function') { + throw createInvalidPackError(path, 'must export a function'); + } + + return new PluginPack({ path, pkg, provider }); +} + +export const createPackAtPath$ = (path) => ( + $from(createPackAtPath(path)) + .map(pack => ({ pack })) + .catch(error => [{ error }]) +); diff --git a/src/plugin_discovery/plugin_pack/packs_in_directory.js b/src/plugin_discovery/plugin_pack/packs_in_directory.js new file mode 100644 index 00000000000000..1caa2c5ad3d44a --- /dev/null +++ b/src/plugin_discovery/plugin_pack/packs_in_directory.js @@ -0,0 +1,30 @@ +import { isInvalidDirectoryError } from '../errors'; + +import { createChildDirectory$ } from './lib'; +import { createPackAtPath$ } from './pack_at_path'; + +/** + * Finds the plugins within a directory. Results are + * an array of objects with either `pack` or `error` + * keys. + * + * - `{ error }` results are provided when the path is not + * a directory, or one of the child directories is not a + * valid plugin pack. + * - `{ pack }` results are for discovered plugins defs + * + * @param {String} path + * @return {Array<{pack}|{error}>} + */ +export const createPacksInDirectory$ = (path) => ( + createChildDirectory$(path) + .mergeMap(createPackAtPath$) + .catch(error => { + if (isInvalidDirectoryError(path)) { + // these are expected errors that we should return as "results" + return [{ error }]; + } + + throw error; + }) +); diff --git a/src/plugin_discovery/plugin_pack/plugin_pack.js b/src/plugin_discovery/plugin_pack/plugin_pack.js new file mode 100644 index 00000000000000..21b1563601e861 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/plugin_pack.js @@ -0,0 +1,54 @@ +import { inspect } from 'util'; + +import { PluginSpec } from '../plugin_spec'; + +export class PluginPack { + constructor({ path, pkg, provider }) { + this._path = path; + this._pkg = pkg; + this._provider = provider; + } + + /** + * Get the contents of this plugin pack's package.json file + * @return {Object} + */ + getPkg() { + return this._pkg; + } + + /** + * Get the absolute path to this plugin pack on disk + * @return {String} + */ + getPath() { + return this._path; + } + + /** + * Invoke the plugin pack's provider to get the list + * of specs defined in this plugin. + * @return {Array} + */ + getPluginSpecs() { + const pack = this; + const api = { + Plugin: class ScopedPluginSpec extends PluginSpec { + constructor(options) { + super(pack, options); + } + } + }; + + const specs = [].concat(this._provider(api) || []); + + // verify that all specs are instances of passed "Plugin" class + specs.forEach(spec => { + if (!(spec instanceof api.Plugin)) { + throw new TypeError('unexpected plugin export ' + inspect(spec)); + } + }); + + return specs; + } +} diff --git a/src/plugin_discovery/plugin_spec/index.js b/src/plugin_discovery/plugin_spec/index.js new file mode 100644 index 00000000000000..550e9514365ba3 --- /dev/null +++ b/src/plugin_discovery/plugin_spec/index.js @@ -0,0 +1 @@ +export { PluginSpec } from './plugin_spec'; diff --git a/src/plugin_discovery/plugin_spec/plugin_spec.js b/src/plugin_discovery/plugin_spec/plugin_spec.js new file mode 100644 index 00000000000000..ff4cee7c4ffd9f --- /dev/null +++ b/src/plugin_discovery/plugin_spec/plugin_spec.js @@ -0,0 +1,167 @@ +import { resolve, basename, isAbsolute as isAbsolutePath } from 'path'; + +import toPath from 'lodash/internal/toPath'; +import { get, noop } from 'lodash'; + +export class PluginSpec { + /** + * @param {PluginPack} pack - The plugin pack that produced this spec + * @param {Object} opts - the options for this plugin + * @param {String} [opts.id=pkg.name] - the id for this plugin. + * @param {Object} [opts.uiExports] - a mapping of UiExport types + * to UI modules or metadata about + * the UI module + * @param {Array} [opts.require] - the other plugins that this plugin + * requires. These plugins must exist and + * be enabled for this plugin to function. + * The require'd plugins will also be + * initialized first, in order to make sure + * that dependencies provided by these plugins + * are available + * @param {String} [opts.version=pkg.version] - the version of this plugin + * @param {Function} [opts.init] - A function that will be called to initialize + * this plugin at the appropriate time. + * @param {Function} [opts.configPrefix=this.id] - The prefix to use for configuration + * values in the main configuration service + * @param {Function} [opts.config] - A function that produces a configuration + * schema using Joi, which is passed as its + * first argument. + * @param {String|False} [opts.publicDir=path + '/public'] + * - the public directory for this plugin. The final directory must + * have the name "public", though it can be located somewhere besides + * the root of the plugin. Set this to false to disable exposure of a + * public directory + */ + constructor(pack, options) { + const { + id, + require, + kibanaVersion, + uiExports, + publicDir, + configPrefix, + config, + deprecations, + preInit, + init, + isEnabled, + } = options; + + this._id = id; + this._pack = pack; + this._kibanaVersion = kibanaVersion; + this._require = require; + + this._publicDir = publicDir; + this._uiExports = uiExports; + + this._configPrefix = configPrefix; + this._configSchemaProvider = config; + this._configDeprecationsProvider = deprecations; + + this._isEnabled = isEnabled; + this._preInit = preInit; + this._init = init; + + if (!this.getId()) { + throw new Error('Unable to determine plugin id'); + } + + if (!this.getVersion()) { + throw new TypeError('Unable to determin plugin version'); + } + + if (!Array.isArray(this.getRequiredPluginIds())) { + throw new TypeError('"plugin.require" must be an array of plugin ids'); + } + + if (this._publicDir) { + if (!isAbsolutePath(this._publicDir)) { + throw new Error('plugin.publicDir must be an absolute path'); + } + if (basename(this._publicDir) !== 'public') { + throw new Error(`publicDir for plugin ${this.getId()} must end with a "public" directory.`); + } + } + } + + getPack() { + return this._pack; + } + + getPkg() { + return this._pack.getPkg(); + } + + getPath() { + return this._pack.getPath(); + } + + getId() { + return this._id || this.getPkg().name; + } + + getVersion() { + return this.getPkg().version; + } + + isEnabled(config) { + if (this._isEnabled) { + return this._isEnabled(config); + } + + return Boolean(this.readConfigValue(config, 'enabled')); + } + + getExpectedKibanaVersion() { + // Plugins must specify their version, and by default that version should match + // the version of kibana down to the patch level. If these two versions need + // to diverge, they can specify a kibana.version in the package to indicate the + // version of kibana the plugin is intended to work with. + return this._kibanaVersion || get(this.getPack().getPkg(), 'kibana.version') || this.getVersion(); + } + + getRequiredPluginIds() { + return this._require || []; + } + + getPublicDir() { + if (this._publicDir === false) { + return null; + } + + if (!this._publicDir) { + return resolve(this.getPack().getPath(), 'public'); + } + + return this._publicDir; + } + + getExportSpecs() { + return this._uiExports || {}; + } + + getPreInitHandler() { + return this._preInit || noop; + } + + getInitHandler() { + return this._init || noop; + } + + getConfigPrefix() { + return this._configPrefix || this.getId(); + } + + getConfigSchemaProvider() { + return this._configSchemaProvider || noop; + } + + readConfigValue(config, key) { + return config.get([...toPath(this.getConfigPrefix()), ...toPath(key)]); + } + + getDeprecationsProvider() { + return this._configDeprecationsProvider || noop; + } +} diff --git a/src/plugin_discovery/ui_export_defaults.js b/src/plugin_discovery/ui_export_defaults.js new file mode 100644 index 00000000000000..7cf5d855ec3cbe --- /dev/null +++ b/src/plugin_discovery/ui_export_defaults.js @@ -0,0 +1,41 @@ +import { dirname, resolve } from 'path'; +const ROOT = dirname(require.resolve('../../package.json')); + +export const UI_EXPORT_DEFAULTS = { + webpackNoParseRules: [ + /node_modules[\/\\](angular|elasticsearch-browser)[\/\\]/, + /node_modules[\/\\](mocha|moment)[\/\\]/ + ], + + webpackAliases: { + ui: resolve(ROOT, 'src/ui/public'), + ui_framework: resolve(ROOT, 'ui_framework'), + packages: resolve(ROOT, 'packages'), + test_harness: resolve(ROOT, 'src/test_harness/public'), + querystring: 'querystring-browser', + moment$: resolve(ROOT, 'webpackShims/moment'), + 'moment-timezone$': resolve(ROOT, 'webpackShims/moment-timezone') + }, + + translationPaths: [ + resolve(ROOT, 'src/ui/ui_i18n/translations/en.json'), + ], + + appExtensions: { + fieldFormatEditors: [ + 'ui/field_format_editor/register' + ], + visRequestHandlers: [ + 'ui/vis/request_handlers/courier', + 'ui/vis/request_handlers/none' + ], + visResponseHandlers: [ + 'ui/vis/response_handlers/basic', + 'ui/vis/response_handlers/none', + 'ui/vis/response_handlers/tabify', + ], + visEditorTypes: [ + 'ui/vis/editors/default/default', + ], + }, +}; diff --git a/src/plugin_discovery/ui_export_types/index.js b/src/plugin_discovery/ui_export_types/index.js new file mode 100644 index 00000000000000..41868d6f64890c --- /dev/null +++ b/src/plugin_discovery/ui_export_types/index.js @@ -0,0 +1,55 @@ +export { + injectDefaultVars, + replaceInjectedVars, +} from './modify_injected_vars'; + +export { + mappings, +} from './saved_object_mappings'; + +export { + app, + apps, +} from './ui_apps'; + +export { + visTypes, + visResponseHandlers, + visRequestHandlers, + visEditorTypes, + savedObjectTypes, + embeddableHandlers, + fieldFormats, + fieldFormatEditors, + spyModes, + chromeNavControls, + navbarExtensions, + managementSections, + devTools, + docViews, + hacks, + visTypeEnhancers, + aliases, +} from './ui_extensions'; + +export { + translations, +} from './ui_i18n'; + +export { + link, + links, +} from './ui_nav_links'; + +export { + uiSettingDefaults, +} from './ui_settings'; + +export { + unknown, +} from './unknown'; + +export { + noParse, + __globalImportAliases__, +} from './webpack_customizations'; diff --git a/src/plugin_discovery/ui_export_types/modify_injected_vars.js b/src/plugin_discovery/ui_export_types/modify_injected_vars.js new file mode 100644 index 00000000000000..364646234a503d --- /dev/null +++ b/src/plugin_discovery/ui_export_types/modify_injected_vars.js @@ -0,0 +1,13 @@ +import { concat } from './reduce'; +import { wrap, alias, mapSpec } from './modify_reduce'; + +export const replaceInjectedVars = wrap(alias('injectedVarsReplacers'), concat); + +export const injectDefaultVars = wrap( + alias('defaultInjectedVarProviders'), + mapSpec((spec, type, pluginSpec) => ({ + pluginSpec, + fn: spec, + })), + concat +); diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/alias.js b/src/plugin_discovery/ui_export_types/modify_reduce/alias.js new file mode 100644 index 00000000000000..8a13fa7a12ae56 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/modify_reduce/alias.js @@ -0,0 +1,10 @@ +/** + * Creates a reducer wrapper which, when called with a reducer, creates a new + * reducer that replaces the `type` value with `newType` before delegating to + * the wrapped reducer + * @param {String} newType + * @return {Function} + */ +export const alias = (newType) => (next) => (acc, spec, type, pluginSpec) => ( + next(acc, spec, newType, pluginSpec) +); diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/debug.js b/src/plugin_discovery/ui_export_types/modify_reduce/debug.js new file mode 100644 index 00000000000000..5c2df3059fae2c --- /dev/null +++ b/src/plugin_discovery/ui_export_types/modify_reduce/debug.js @@ -0,0 +1,12 @@ +import { mapSpec } from './map_spec'; + +/** + * Reducer wrapper which, replaces the `spec` with the details about the definition + * of that spec + * @type {Function} + */ +export const debug = mapSpec((spec, type, pluginSpec) => ({ + spec, + type, + pluginSpec +})); diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/index.js b/src/plugin_discovery/ui_export_types/modify_reduce/index.js new file mode 100644 index 00000000000000..9c698f304ec6e3 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/modify_reduce/index.js @@ -0,0 +1,5 @@ +export { alias } from './alias'; +export { debug } from './debug'; +export { mapSpec } from './map_spec'; +export { wrap } from './wrap'; +export { uniqueKeys } from './unique_keys'; diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/map_spec.js b/src/plugin_discovery/ui_export_types/modify_reduce/map_spec.js new file mode 100644 index 00000000000000..3607e8403c4f53 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/modify_reduce/map_spec.js @@ -0,0 +1,11 @@ +/** + * Creates a reducer wrapper which, when called with a reducer, creates a new + * reducer that replaces the `specs` value with the result of calling + * `mapFn(spec, type, pluginSpec)` before delegating to the wrapped + * reducer + * @param {Function} mapFn receives `(specs, type, pluginSpec)` + * @return {Function} + */ +export const mapSpec = (mapFn) => (next) => (acc, spec, type, pluginSpec) => ( + next(acc, mapFn(spec, type, pluginSpec), type, pluginSpec) +); diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/unique_keys.js b/src/plugin_discovery/ui_export_types/modify_reduce/unique_keys.js new file mode 100644 index 00000000000000..4d7f3650710f2c --- /dev/null +++ b/src/plugin_discovery/ui_export_types/modify_reduce/unique_keys.js @@ -0,0 +1,10 @@ +export const uniqueKeys = (sourceType) => (next) => (acc, spec, type, pluginSpec) => { + const duplicates = Object.keys(spec) + .filter(key => acc[type] && acc[type].hasOwnProperty(key)); + + if (duplicates.length) { + throw new Error(`${pluginSpec.id()} defined duplicate ${sourceType || type} values: ${duplicates}`); + } + + return next(acc, spec, type, pluginSpec); +}; diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/wrap.js b/src/plugin_discovery/ui_export_types/modify_reduce/wrap.js new file mode 100644 index 00000000000000..26e26709dd342a --- /dev/null +++ b/src/plugin_discovery/ui_export_types/modify_reduce/wrap.js @@ -0,0 +1,9 @@ +/** + * Wrap a reducer + * @param {Function} ...wrappers + * @param {Function} reducer + * @return {Function} + */ +export function wrap(...args) { + return args.reverse().reduce((reducer, decorate) => decorate(reducer)); +} diff --git a/src/plugin_discovery/ui_export_types/reduce/concat.js b/src/plugin_discovery/ui_export_types/reduce/concat.js new file mode 100644 index 00000000000000..5346a2a09a3268 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/reduce/concat.js @@ -0,0 +1,11 @@ +import { mergeType } from './merge_type'; + +/** + * Reducer that merges two values concatenating all values + * into a flattened array + * @param {Any} [initial] + * @return {Function} + */ +export const concat = mergeType((a, b) => ( + [].concat(a || [], b || []) +)); diff --git a/src/plugin_discovery/ui_export_types/reduce/concat_values.js b/src/plugin_discovery/ui_export_types/reduce/concat_values.js new file mode 100644 index 00000000000000..5606b58f5e039d --- /dev/null +++ b/src/plugin_discovery/ui_export_types/reduce/concat_values.js @@ -0,0 +1,15 @@ +import { assign } from 'lodash'; + +import { mergeType } from './merge_type'; + +/** + * Creates a reducer that merges specs by concatenating the values of + * all keys in accumulator and spec with the same logic as concat + * @param {[type]} initial [description] + * @return {[type]} [description] + */ +export const concatValues = mergeType((objectA, objectB) => ( + assign({}, objectA || {}, objectB || {}, (a, b) => ( + [].concat(a || [], b || []) + )) +)); diff --git a/src/plugin_discovery/ui_export_types/reduce/index.js b/src/plugin_discovery/ui_export_types/reduce/index.js new file mode 100644 index 00000000000000..f92630d7c0b653 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/reduce/index.js @@ -0,0 +1,3 @@ +export { concatValues } from './concat_values'; +export { concat } from './concat'; +export { merge } from './merge'; diff --git a/src/plugin_discovery/ui_export_types/reduce/merge.js b/src/plugin_discovery/ui_export_types/reduce/merge.js new file mode 100644 index 00000000000000..da7d3226acfa44 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/reduce/merge.js @@ -0,0 +1,6 @@ +import { mergeType } from './merge_type'; + +export const merge = mergeType((a, b) => ({ + ...a, + ...b +})); diff --git a/src/plugin_discovery/ui_export_types/reduce/merge_type.js b/src/plugin_discovery/ui_export_types/reduce/merge_type.js new file mode 100644 index 00000000000000..87b52a44b2cfb6 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/reduce/merge_type.js @@ -0,0 +1,10 @@ +/** + * Creates a reducer that merges the values within `acc[type]` by calling + * `merge` with `(acc[type], spec, type, pluginSpec)` + * @param {Function} merge receives `(acc[type], spec, type, pluginSpec)` + * @return {Function} + */ +export const mergeType = (merge) => (acc, spec, type, pluginSpec) => ({ + ...acc, + [type]: merge(acc[type], spec, type, pluginSpec) +}); diff --git a/src/plugin_discovery/ui_export_types/reduce/unique_assign.js b/src/plugin_discovery/ui_export_types/reduce/unique_assign.js new file mode 100644 index 00000000000000..11313fe1f8ecc0 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/reduce/unique_assign.js @@ -0,0 +1,12 @@ +export function uniqueAssign(acc, spec, type, pluginSpec) { + return Object.keys(spec).reduce((acc, key) => { + if (acc.hasOwnProperty(key)) { + throw new Error(`${pluginSpec.getId()} defines a duplicate ${type} for key ${key}`); + } + + return { + ...acc, + [key]: spec[key] + }; + }, acc); +} diff --git a/src/plugin_discovery/ui_export_types/saved_object_mappings.js b/src/plugin_discovery/ui_export_types/saved_object_mappings.js new file mode 100644 index 00000000000000..6b9cf63ec95d6c --- /dev/null +++ b/src/plugin_discovery/ui_export_types/saved_object_mappings.js @@ -0,0 +1,5 @@ +import { concat } from './reduce'; +import { wrap, debug } from './modify_reduce'; + +// mapping types +export const mappings = wrap(debug, concat); diff --git a/src/plugin_discovery/ui_export_types/ui_apps.js b/src/plugin_discovery/ui_export_types/ui_apps.js new file mode 100644 index 00000000000000..07203fe97cf4c7 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/ui_apps.js @@ -0,0 +1,47 @@ +import { noop, uniq } from 'lodash'; + +import { concat } from './reduce'; +import { alias, mapSpec, wrap } from './modify_reduce'; + +function applySpecDefaults(spec, type, pluginSpec) { + const pluginId = pluginSpec.getId(); + const { + id = pluginId, + main, + title, + order = 0, + description = '', + icon, + hidden = false, + linkToLastSubUrl = true, + listed = !hidden, + templateName = 'ui_app', + injectVars = noop, + url = `/app/${id}`, + uses = [], + } = spec; + + return { + pluginId, + id, + main, + title, + order, + description, + icon, + hidden, + linkToLastSubUrl, + listed, + templateName, + injectVars, + url, + uses: uniq([ + ...uses, + 'hacks', + 'chromeNavControls' + ]), + }; +} + +export const apps = wrap(alias('uiAppSpecs'), mapSpec(applySpecDefaults), concat); +export const app = wrap(alias('uiAppSpecs'), mapSpec(applySpecDefaults), concat); diff --git a/src/plugin_discovery/ui_export_types/ui_extensions.js b/src/plugin_discovery/ui_export_types/ui_extensions.js new file mode 100644 index 00000000000000..52f7492dee6cf6 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/ui_extensions.js @@ -0,0 +1,38 @@ +import { concatValues } from './reduce'; +import { mapSpec, alias, wrap } from './modify_reduce'; + +/** + * Reducer "preset" that merges named "first-class" appExtensions by + * converting them into objects and then concatenating the values of those objects + * @type {Function} + */ +const appExtension = wrap( + mapSpec((spec, type) => ({ [type]: spec })), + alias('appExtensions'), + concatValues +); + +// plain extension groups produce lists of modules that will be required by the entry +// files to include extensions of specific types into specific apps +export const visTypes = appExtension; +export const visResponseHandlers = appExtension; +export const visRequestHandlers = appExtension; +export const visEditorTypes = appExtension; +export const savedObjectTypes = appExtension; +export const embeddableHandlers = appExtension; +export const fieldFormats = appExtension; +export const fieldFormatEditors = appExtension; +export const spyModes = appExtension; +export const chromeNavControls = appExtension; +export const navbarExtensions = appExtension; +export const managementSections = appExtension; +export const devTools = appExtension; +export const docViews = appExtension; +export const hacks = appExtension; + +// aliases visTypeEnhancers to the visTypes group +export const visTypeEnhancers = wrap(alias('visTypes'), appExtension); + +// adhoc extension groups can define new extension groups on the fly +// so that plugins could concat their own +export const aliases = concatValues; diff --git a/src/plugin_discovery/ui_export_types/ui_i18n.js b/src/plugin_discovery/ui_export_types/ui_i18n.js new file mode 100644 index 00000000000000..9e589583394c8e --- /dev/null +++ b/src/plugin_discovery/ui_export_types/ui_i18n.js @@ -0,0 +1,5 @@ +import { concat } from './reduce'; +import { wrap, alias } from './modify_reduce'; + +// paths to translation files +export const translations = wrap(alias('translationPaths'), concat); diff --git a/src/plugin_discovery/ui_export_types/ui_nav_links.js b/src/plugin_discovery/ui_export_types/ui_nav_links.js new file mode 100644 index 00000000000000..9a9a3841a988e7 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/ui_nav_links.js @@ -0,0 +1,5 @@ +import { concat } from './reduce'; +import { wrap, alias } from './modify_reduce'; + +export const links = wrap(alias('navLinkSpecs'), concat); +export const link = wrap(alias('navLinkSpecs'), concat); diff --git a/src/plugin_discovery/ui_export_types/ui_settings.js b/src/plugin_discovery/ui_export_types/ui_settings.js new file mode 100644 index 00000000000000..b522e89f9e1fa9 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/ui_settings.js @@ -0,0 +1,4 @@ +import { merge } from './reduce'; +import { wrap, uniqueKeys } from './modify_reduce'; + +export const uiSettingDefaults = wrap(uniqueKeys(), merge); diff --git a/src/plugin_discovery/ui_export_types/unknown.js b/src/plugin_discovery/ui_export_types/unknown.js new file mode 100644 index 00000000000000..778d89c16f28d3 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/unknown.js @@ -0,0 +1,4 @@ +import { concat } from './reduce'; +import { wrap, alias, debug } from './modify_reduce'; + +export const unknown = wrap(debug, alias('unknown'), concat); diff --git a/src/plugin_discovery/ui_export_types/webpack_customizations.js b/src/plugin_discovery/ui_export_types/webpack_customizations.js new file mode 100644 index 00000000000000..0939d71eb054f7 --- /dev/null +++ b/src/plugin_discovery/ui_export_types/webpack_customizations.js @@ -0,0 +1,5 @@ +import { concat, merge } from './reduce'; +import { alias, wrap, uniqueKeys } from './modify_reduce'; + +export const noParse = wrap(alias('webpackNoParseRules'), concat); +export const __globalImportAliases__ = wrap(alias('webpackAliases'), uniqueKeys('__globalImportAliases__'), merge); diff --git a/src/plugin_discovery/utils/combine_latest.js b/src/plugin_discovery/utils/combine_latest.js new file mode 100644 index 00000000000000..3ae147004b0255 --- /dev/null +++ b/src/plugin_discovery/utils/combine_latest.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $combineLatest = (...args) => ( + Rx.Observable.combineLatest(...args) +); diff --git a/src/plugin_discovery/utils/concat.js b/src/plugin_discovery/utils/concat.js new file mode 100644 index 00000000000000..9fdadcf32120e1 --- /dev/null +++ b/src/plugin_discovery/utils/concat.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $concat = (...args) => ( + Rx.Observable.$concat(...args) +); diff --git a/src/plugin_discovery/utils/create.js b/src/plugin_discovery/utils/create.js new file mode 100644 index 00000000000000..af34ae6f46bc32 --- /dev/null +++ b/src/plugin_discovery/utils/create.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $create = (block) => ( + Rx.Observable.create(block) +); diff --git a/src/plugin_discovery/utils/debug.js b/src/plugin_discovery/utils/debug.js new file mode 100644 index 00000000000000..a81d9d13badbea --- /dev/null +++ b/src/plugin_discovery/utils/debug.js @@ -0,0 +1,13 @@ +export function debug(name) { + return { + next(v) { + console.log('N: %s %s', name, v); + }, + error(error) { + console.log('E: %s', name, error); + }, + complete() { + console.log('C: %s', name); + } + }; +} diff --git a/src/plugin_discovery/utils/defer.js b/src/plugin_discovery/utils/defer.js new file mode 100644 index 00000000000000..2ee970ccd73bae --- /dev/null +++ b/src/plugin_discovery/utils/defer.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $defer = (...args) => ( + Rx.Observable.defer(...args) +); diff --git a/src/plugin_discovery/utils/empty.js b/src/plugin_discovery/utils/empty.js new file mode 100644 index 00000000000000..e3bf3916cf348f --- /dev/null +++ b/src/plugin_discovery/utils/empty.js @@ -0,0 +1,3 @@ +import Rx from 'rxjs/Rx'; + +export const empty$ = Rx.Observable.empty(); diff --git a/src/plugin_discovery/utils/fcb.js b/src/plugin_discovery/utils/fcb.js new file mode 100644 index 00000000000000..eaf43d1526c762 --- /dev/null +++ b/src/plugin_discovery/utils/fcb.js @@ -0,0 +1,14 @@ +import { $create } from './create'; + +export const $fcb = (block) => ( + $create(observer => { + block((error, value) => { + if (error) { + observer.error(error); + } else { + observer.next(value); + observer.complete(observer); + } + }); + }) +); diff --git a/src/plugin_discovery/utils/from.js b/src/plugin_discovery/utils/from.js new file mode 100644 index 00000000000000..51490bf4d40982 --- /dev/null +++ b/src/plugin_discovery/utils/from.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $from = (input) => ( + Rx.Observable.from(input) +); diff --git a/src/plugin_discovery/utils/from_event.js b/src/plugin_discovery/utils/from_event.js new file mode 100644 index 00000000000000..f7d37b618eec8c --- /dev/null +++ b/src/plugin_discovery/utils/from_event.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $fromEvent = (...args) => ( + Rx.Observable.fromEvent(...args) +); diff --git a/src/plugin_discovery/utils/from_promise.js b/src/plugin_discovery/utils/from_promise.js new file mode 100644 index 00000000000000..c29c70fe292694 --- /dev/null +++ b/src/plugin_discovery/utils/from_promise.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $fromPromise = (...args) => ( + Rx.Observable.fromPromise(...args) +); diff --git a/src/plugin_discovery/utils/index.js b/src/plugin_discovery/utils/index.js new file mode 100644 index 00000000000000..64397b8dd1ab33 --- /dev/null +++ b/src/plugin_discovery/utils/index.js @@ -0,0 +1,14 @@ +export { $combineLatest } from './combine_latest'; +export { $concat } from './concat'; +export { $create } from './create'; +export { $defer } from './defer'; +export { empty$ } from './empty'; +export { $fcb } from './fcb'; +export { $from } from './from'; +export { $fromEvent } from './from_event'; +export { $fromPromise } from './from_promise'; +export { $merge } from './merge'; +export { $of } from './of'; +export { $race } from './race'; +export { $timer } from './timer'; +export { $throw } from './throw'; diff --git a/src/plugin_discovery/utils/merge.js b/src/plugin_discovery/utils/merge.js new file mode 100644 index 00000000000000..09d1cce19e7b86 --- /dev/null +++ b/src/plugin_discovery/utils/merge.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $merge = (...args) => ( + Rx.Observable.merge(...args) +); diff --git a/src/plugin_discovery/utils/of.js b/src/plugin_discovery/utils/of.js new file mode 100644 index 00000000000000..ad9c4cc2dec279 --- /dev/null +++ b/src/plugin_discovery/utils/of.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $of = (...args) => ( + Rx.Observable.of(...args) +); diff --git a/src/plugin_discovery/utils/race.js b/src/plugin_discovery/utils/race.js new file mode 100644 index 00000000000000..18dcf4bfd193a5 --- /dev/null +++ b/src/plugin_discovery/utils/race.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $race = (...args) => ( + Rx.Observable.race(...args) +); diff --git a/src/plugin_discovery/utils/throw.js b/src/plugin_discovery/utils/throw.js new file mode 100644 index 00000000000000..8935307a66d749 --- /dev/null +++ b/src/plugin_discovery/utils/throw.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $throw = (...args) => ( + Rx.Observable.throw(...args) +); diff --git a/src/plugin_discovery/utils/timer.js b/src/plugin_discovery/utils/timer.js new file mode 100644 index 00000000000000..8bf2069adb7766 --- /dev/null +++ b/src/plugin_discovery/utils/timer.js @@ -0,0 +1,5 @@ +import Rx from 'rxjs/Rx'; + +export const $timer = (...args) => ( + Rx.Observable.timer(...args) +); diff --git a/src/server/config/__tests__/config.js b/src/server/config/__tests__/config.js index 200140adca6c86..53733b52136c2c 100644 --- a/src/server/config/__tests__/config.js +++ b/src/server/config/__tests__/config.js @@ -1,4 +1,4 @@ -import Config from '../config'; +import { Config } from '../config'; import expect from 'expect.js'; import _ from 'lodash'; import Joi from 'joi'; diff --git a/src/server/config/config.js b/src/server/config/config.js index c743fcd45f8b90..61276a645f7deb 100644 --- a/src/server/config/config.js +++ b/src/server/config/config.js @@ -8,7 +8,7 @@ const schema = Symbol('Joi Schema'); const schemaExts = Symbol('Schema Extensions'); const vals = Symbol('config values'); -export default class Config { +export class Config { static withDefaultSchema(settings = {}) { return new Config(createDefaultSchema(), settings); } diff --git a/src/server/config/index.js b/src/server/config/index.js new file mode 100644 index 00000000000000..88b7273fd16282 --- /dev/null +++ b/src/server/config/index.js @@ -0,0 +1,2 @@ +export { transformDeprecations } from './transform_deprecations'; +export { Config } from './config'; diff --git a/src/server/config/setup.js b/src/server/config/setup.js index bcdca730386ad7..d6ecbe562cf56d 100644 --- a/src/server/config/setup.js +++ b/src/server/config/setup.js @@ -1,4 +1,4 @@ -import Config from './config'; +import { Config } from './config'; import { transformDeprecations } from './transform_deprecations'; export default function (kbnServer) { From b1947e13a2a729efd1034af204af954cd038037d Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 13:45:37 -0700 Subject: [PATCH 02/67] integrate plugin discovery module with server --- src/cli/cluster/base_path_proxy.js | 2 +- src/core_plugins/console/index.js | 10 +- src/core_plugins/kibana/index.js | 4 + .../state_session_storage_redirect/index.js | 2 +- src/core_plugins/tests_bundle/index.js | 52 ++-- .../tests_bundle/tests_entry_template.js | 22 +- src/optimize/base_optimizer.js | 21 +- src/optimize/index.js | 19 +- src/optimize/lazy/lazy_optimizer.js | 12 +- src/optimize/lazy/optmzr_role.js | 3 +- src/plugin_discovery/index.js | 3 +- .../ui_export_types/webpack_customizations.js | 5 - src/server/http/index.js | 2 +- src/server/kbn_server.js | 49 ++-- src/server/plugins/check_enabled.js | 25 -- src/server/plugins/check_version.js | 36 --- src/server/plugins/check_versions_mixin.js | 32 +++ src/server/plugins/index.js | 4 + src/server/plugins/initialize.js | 22 -- src/server/plugins/initialize_mixin.js | 27 +++ src/server/plugins/lib/call_plugin_hook.js | 31 +++ src/server/plugins/lib/index.js | 3 + .../plugins/lib/is_version_compatible.js | 17 ++ src/server/plugins/lib/plugin.js | 106 ++++++++ src/server/plugins/plugin.js | 175 -------------- src/server/plugins/plugin_api.js | 30 --- src/server/plugins/plugin_collection.js | 78 ------ src/server/plugins/plugin_init.js | 35 --- src/server/plugins/scan.js | 59 ----- src/server/plugins/scan_mixin.js | 51 ++++ src/server/plugins/wait_for_plugins_init.js | 28 +++ src/server/status/index.js | 23 +- src/ui/app_entry_template.js | 29 --- .../field_formats_mixin.js | 2 +- src/ui/field_formats/index.js | 2 + src/ui/index.js | 139 +---------- .../angular-bootstrap/bindHtml/bindHtml.js | 2 +- src/ui/public/assets/favicons/manifest.json | 2 +- src/ui/public/filter_bar/filter_bar.js | 3 - .../saved_objects/saved_objects_client.js | 8 +- src/ui/ui_app.js | 69 ------ src/ui/ui_app_collection.js | 45 ---- src/ui/ui_apps/index.js | 1 + src/ui/ui_apps/ui_app.js | 126 ++++++++++ src/ui/ui_apps/ui_apps_mixin.js | 38 +++ src/ui/ui_bundle.js | 68 ------ src/ui/ui_bundle_collection.js | 108 --------- src/ui/ui_bundler_env.js | 99 -------- src/ui/ui_bundles/app_entry_template.js | 14 ++ src/ui/ui_bundles/index.js | 1 + src/ui/ui_bundles/ui_bundle.js | 86 +++++++ src/ui/ui_bundles/ui_bundles_controller.js | 183 ++++++++++++++ src/ui/ui_bundles/ui_bundles_mixin.js | 12 + src/ui/ui_exports.js | 197 --------------- .../__tests__/collect_ui_exports.js | 3 +- .../ui_exports}/collect_ui_exports.js | 2 +- src/ui/ui_exports/index.js | 1 + .../ui_exports}/ui_export_defaults.js | 2 +- .../ui_exports}/ui_export_types/index.js | 0 .../ui_export_types/modify_injected_vars.js | 0 .../ui_export_types/modify_reduce/alias.js | 0 .../ui_export_types/modify_reduce/debug.js | 0 .../ui_export_types/modify_reduce/index.js | 0 .../ui_export_types/modify_reduce/map_spec.js | 0 .../modify_reduce/unique_keys.js | 0 .../ui_export_types/modify_reduce/wrap.js | 0 .../ui_export_types/reduce/concat.js | 0 .../ui_export_types/reduce/concat_values.js | 0 .../ui_export_types/reduce/index.js | 0 .../ui_export_types/reduce/merge.js | 0 .../ui_export_types/reduce/merge_type.js | 0 .../ui_export_types/reduce/unique_assign.js | 0 .../ui_export_types/saved_object_mappings.js | 0 .../ui_exports}/ui_export_types/ui_apps.js | 0 .../ui_export_types/ui_extensions.js | 1 + .../ui_exports}/ui_export_types/ui_i18n.js | 0 .../ui_export_types/ui_nav_links.js | 0 .../ui_export_types/ui_settings.js | 0 .../ui_exports}/ui_export_types/unknown.js | 0 .../ui_export_types/webpack_customizations.js | 23 ++ src/ui/ui_i18n.js | 66 ----- .../translations/test_plugin_1/de.json | 4 + .../translations/test_plugin_1/en.json | 6 + .../translations/test_plugin_1/es-ES.json | 3 + .../translations/test_plugin_2/de.json | 3 + .../translations/test_plugin_2/en.json | 6 + src/ui/ui_i18n/__tests__/i18n.js | 226 ++++++++++++++++++ src/ui/ui_i18n/i18n.js | 136 +++++++++++ src/ui/ui_i18n/index.js | 2 + src/ui/ui_i18n/translations/en.json | 4 + src/ui/ui_i18n/ui_i18n_mixin.js | 49 ++++ src/ui/ui_mixin.js | 22 ++ src/ui/ui_nav_link.js | 15 -- src/ui/ui_nav_link_collection.js | 34 --- src/ui/ui_nav_links/index.js | 2 + src/ui/ui_nav_links/ui_nav_link.js | 49 ++++ src/ui/ui_nav_links/ui_nav_links_mixin.js | 23 ++ src/ui/ui_render/index.js | 1 + src/ui/ui_render/ui_render_mixin.js | 128 ++++++++++ src/ui/{ => ui_render}/views/chrome.jade | 0 .../{ => ui_render}/views/root_redirect.jade | 0 src/ui/{ => ui_render}/views/ui_app.jade | 4 +- .../ui_settings_mixin_integration.js | 2 +- src/ui/ui_settings/ui_settings_mixin.js | 7 +- 104 files changed, 1583 insertions(+), 1463 deletions(-) delete mode 100644 src/plugin_discovery/ui_export_types/webpack_customizations.js delete mode 100644 src/server/plugins/check_enabled.js delete mode 100644 src/server/plugins/check_version.js create mode 100644 src/server/plugins/check_versions_mixin.js create mode 100644 src/server/plugins/index.js delete mode 100644 src/server/plugins/initialize.js create mode 100644 src/server/plugins/initialize_mixin.js create mode 100644 src/server/plugins/lib/call_plugin_hook.js create mode 100644 src/server/plugins/lib/index.js create mode 100644 src/server/plugins/lib/is_version_compatible.js create mode 100644 src/server/plugins/lib/plugin.js delete mode 100644 src/server/plugins/plugin.js delete mode 100644 src/server/plugins/plugin_api.js delete mode 100644 src/server/plugins/plugin_collection.js delete mode 100644 src/server/plugins/plugin_init.js delete mode 100644 src/server/plugins/scan.js create mode 100644 src/server/plugins/scan_mixin.js create mode 100644 src/server/plugins/wait_for_plugins_init.js delete mode 100644 src/ui/app_entry_template.js rename src/ui/{ => field_formats}/field_formats_mixin.js (93%) create mode 100644 src/ui/field_formats/index.js delete mode 100644 src/ui/ui_app.js delete mode 100644 src/ui/ui_app_collection.js create mode 100644 src/ui/ui_apps/index.js create mode 100644 src/ui/ui_apps/ui_app.js create mode 100644 src/ui/ui_apps/ui_apps_mixin.js delete mode 100644 src/ui/ui_bundle.js delete mode 100644 src/ui/ui_bundle_collection.js delete mode 100644 src/ui/ui_bundler_env.js create mode 100644 src/ui/ui_bundles/app_entry_template.js create mode 100644 src/ui/ui_bundles/index.js create mode 100644 src/ui/ui_bundles/ui_bundle.js create mode 100644 src/ui/ui_bundles/ui_bundles_controller.js create mode 100644 src/ui/ui_bundles/ui_bundles_mixin.js delete mode 100644 src/ui/ui_exports.js rename src/{plugin_discovery => ui/ui_exports}/__tests__/collect_ui_exports.js (95%) rename src/{plugin_discovery => ui/ui_exports}/collect_ui_exports.js (81%) create mode 100644 src/ui/ui_exports/index.js rename src/{plugin_discovery => ui/ui_exports}/ui_export_defaults.js (94%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/index.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/modify_injected_vars.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/modify_reduce/alias.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/modify_reduce/debug.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/modify_reduce/index.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/modify_reduce/map_spec.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/modify_reduce/unique_keys.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/modify_reduce/wrap.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/reduce/concat.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/reduce/concat_values.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/reduce/index.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/reduce/merge.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/reduce/merge_type.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/reduce/unique_assign.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/saved_object_mappings.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/ui_apps.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/ui_extensions.js (97%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/ui_i18n.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/ui_nav_links.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/ui_settings.js (100%) rename src/{plugin_discovery => ui/ui_exports}/ui_export_types/unknown.js (100%) create mode 100644 src/ui/ui_exports/ui_export_types/webpack_customizations.js delete mode 100644 src/ui/ui_i18n.js create mode 100644 src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/de.json create mode 100644 src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/en.json create mode 100644 src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/es-ES.json create mode 100644 src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_2/de.json create mode 100644 src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_2/en.json create mode 100644 src/ui/ui_i18n/__tests__/i18n.js create mode 100644 src/ui/ui_i18n/i18n.js create mode 100644 src/ui/ui_i18n/index.js create mode 100644 src/ui/ui_i18n/translations/en.json create mode 100644 src/ui/ui_i18n/ui_i18n_mixin.js create mode 100644 src/ui/ui_mixin.js delete mode 100644 src/ui/ui_nav_link.js delete mode 100644 src/ui/ui_nav_link_collection.js create mode 100644 src/ui/ui_nav_links/index.js create mode 100644 src/ui/ui_nav_links/ui_nav_link.js create mode 100644 src/ui/ui_nav_links/ui_nav_links_mixin.js create mode 100644 src/ui/ui_render/index.js create mode 100644 src/ui/ui_render/ui_render_mixin.js rename src/ui/{ => ui_render}/views/chrome.jade (100%) rename src/ui/{ => ui_render}/views/root_redirect.jade (100%) rename src/ui/{ => ui_render}/views/ui_app.jade (98%) diff --git a/src/cli/cluster/base_path_proxy.js b/src/cli/cluster/base_path_proxy.js index 144090b12f6709..5858c339de5bbc 100644 --- a/src/cli/cluster/base_path_proxy.js +++ b/src/cli/cluster/base_path_proxy.js @@ -6,7 +6,7 @@ import { map as promiseMap, fromNode } from 'bluebird'; import { Agent as HttpsAgent } from 'https'; import { readFileSync } from 'fs'; -import Config from '../../server/config/config'; +import { Config } from '../../server/config/config'; import setupConnection from '../../server/http/setup_connection'; import registerHapiPlugins from '../../server/http/register_hapi_plugins'; import setupLogging from '../../server/logging'; diff --git a/src/core_plugins/console/index.js b/src/core_plugins/console/index.js index 27faf5fe3fe96a..688481f48548d8 100644 --- a/src/core_plugins/console/index.js +++ b/src/core_plugins/console/index.js @@ -31,6 +31,14 @@ export default function (kibana) { id: 'console', require: [ 'elasticsearch' ], + isEnabled(config) { + // console must be disabled when tribe mode is configured + return ( + config.get('console.enabled') && + !config.get('elasticsearch.tribe.url') + ); + }, + config: function (Joi) { return Joi.object({ enabled: Joi.boolean().default(true), @@ -115,7 +123,7 @@ export default function (kibana) { } }); - const testApp = kibana.uiExports.apps.hidden.byId['sense-tests']; + const testApp = server.getHiddenUiAppById('sense-tests'); if (testApp) { server.route({ path: '/app/sense-tests', diff --git a/src/core_plugins/kibana/index.js b/src/core_plugins/kibana/index.js index 2ce1b2624b0cc9..33cbec9a157007 100644 --- a/src/core_plugins/kibana/index.js +++ b/src/core_plugins/kibana/index.js @@ -40,6 +40,10 @@ export default function (kibana) { 'plugins/kibana/discover/saved_searches/saved_search_register', 'plugins/kibana/dashboard/saved_dashboard/saved_dashboard_register', ], + embeddableHandlers: [ + 'plugins/kibana/visualize/embeddable/visualize_embeddable_handler_provider', + 'plugins/kibana/discover/embeddable/search_embeddable_handler_provider', + ], app: { id: 'kibana', title: 'Kibana', diff --git a/src/core_plugins/state_session_storage_redirect/index.js b/src/core_plugins/state_session_storage_redirect/index.js index ba0fcfa844f242..37e37ba3928e32 100644 --- a/src/core_plugins/state_session_storage_redirect/index.js +++ b/src/core_plugins/state_session_storage_redirect/index.js @@ -6,7 +6,7 @@ export default function (kibana) { title: 'Redirecting', id: 'stateSessionStorageRedirect', main: 'plugins/state_session_storage_redirect', - listed: false, + hidden: true, } } }); diff --git a/src/core_plugins/tests_bundle/index.js b/src/core_plugins/tests_bundle/index.js index 223ef39f9536ee..4852c649719ab5 100644 --- a/src/core_plugins/tests_bundle/index.js +++ b/src/core_plugins/tests_bundle/index.js @@ -1,7 +1,10 @@ import { union } from 'lodash'; -import findSourceFiles from './find_source_files'; + import { fromRoot } from '../../utils'; +import findSourceFiles from './find_source_files'; +import { createTestEntryTemplate } from './tests_entry_template'; + export default (kibana) => { return new kibana.Plugin({ config: (Joi) => { @@ -13,9 +16,18 @@ export default (kibana) => { }, uiExports: { - bundle: async (UiBundle, env, apps, plugins) => { + async __bundleProvider__(kbnServer) { let modules = []; - const config = kibana.config; + + const { + config, + uiApps, + uiBundles, + plugins, + uiExports: { + uiSettingDefaults = {} + } + } = kbnServer; const testGlobs = [ 'src/ui/public/**/*.js', @@ -26,20 +38,25 @@ export default (kibana) => { if (testingPluginIds) { testGlobs.push('!src/ui/public/**/__tests__/**/*'); testingPluginIds.split(',').forEach((pluginId) => { - const plugin = plugins.byId[pluginId]; - if (!plugin) throw new Error('Invalid testingPluginId :: unknown plugin ' + pluginId); + const plugin = plugins + .find(plugin => plugin.id === pluginId); + + if (!plugin) { + throw new Error('Invalid testingPluginId :: unknown plugin ' + pluginId); + } // add the modules from all of this plugins apps - for (const app of plugin.apps) { - modules = union(modules, app.getModules()); + for (const app of uiApps) { + if (app.getPluginId() === pluginId) { + modules = union(modules, app.getModules()); + } } testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`); }); } else { - // add the modules from all of the apps - for (const app of apps) { + for (const app of uiApps) { modules = union(modules, app.getModules()); } @@ -52,24 +69,17 @@ export default (kibana) => { for (const f of testFiles) modules.push(f); if (config.get('tests_bundle.instrument')) { - env.addPostLoader({ + uiBundles.addPostLoader({ test: /\.js$/, exclude: /[\/\\](__tests__|node_modules|bower_components|webpackShims)[\/\\]/, - loader: 'istanbul-instrumenter' + loader: 'istanbul-instrumenter-loader' }); } - 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({ + uiBundles.add({ id: 'tests', - modules: modules, - template: require('./tests_entry_template'), - env: env + modules, + template: createTestEntryTemplate(uiSettingDefaults), }); }, diff --git a/src/core_plugins/tests_bundle/tests_entry_template.js b/src/core_plugins/tests_bundle/tests_entry_template.js index dfe1157c071fe8..76f0407f56add9 100644 --- a/src/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/core_plugins/tests_bundle/tests_entry_template.js @@ -1,24 +1,12 @@ import { esTestConfig } from '../../test_utils/es'; -export default function ({ env, bundle }) { - - const pluginSlug = env.pluginInfo.sort() - .map(p => ' * - ' + p) - .join('\n'); - - const requires = bundle.modules - .map(m => `require(${JSON.stringify(m)});`) - .join('\n'); - - return ` +export const createTestEntryTemplate = (defaultUiSettings) => (bundle) => ` /** * Test entry file * * This is programatically created and updated, do not modify * - * context: ${JSON.stringify(env.context)} - * includes code from: -${pluginSlug} + * context: ${bundle.getContext()} * */ @@ -47,14 +35,12 @@ window.__KBN__ = { } }, uiSettings: { - defaults: ${JSON.stringify(env.defaultUiSettings, null, 2).split('\n').join('\n ')}, + defaults: ${JSON.stringify(defaultUiSettings, null, 2).split('\n').join('\n ')}, user: {} } }; require('ui/test_harness'); -${requires} +${bundle.getRequires().join('\n')} require('ui/test_harness').bootstrap(/* go! */); `; - -} diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 013b8007be3389..e55469634db2fb 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -25,8 +25,7 @@ const BABEL_EXCLUDE_RE = [ export default class BaseOptimizer { constructor(opts) { - this.env = opts.env; - this.bundles = opts.bundles; + this.uiBundles = opts.uiBundles; this.profile = opts.profile || false; switch (opts.sourceMaps) { @@ -60,7 +59,7 @@ export default class BaseOptimizer { this.compiler.plugin('done', stats => { if (!this.profile) return; - const path = resolve(this.env.workingDir, 'stats.json'); + const path = this.uiBundles.resolvePath('stats.json'); const content = JSON.stringify(stats.toJson()); writeFile(path, content, function (err) { if (err) throw err; @@ -71,7 +70,7 @@ export default class BaseOptimizer { } getConfig() { - const cacheDirectory = resolve(this.env.workingDir, '../.cache', this.bundles.hashBundleEntries()); + const cacheDirectory = this.uiBundles.getCachePath(); function getStyleLoaders(preProcessors = [], postProcessors = []) { return ExtractTextPlugin.extract({ @@ -105,13 +104,13 @@ export default class BaseOptimizer { const commonConfig = { node: { fs: 'empty' }, context: fromRoot('.'), - entry: this.bundles.toWebpackEntries(), + entry: this.uiBundles.toWebpackEntries(), devtool: this.sourceMaps, profile: this.profile || false, output: { - path: this.env.workingDir, + path: this.uiBundles.getWorkingDir(), filename: '[name].bundle.js', sourceMapFilename: '[file].map', publicPath: PUBLIC_PATH_PLACEHOLDER, @@ -168,7 +167,7 @@ export default class BaseOptimizer { }, { test: /\.js$/, - exclude: BABEL_EXCLUDE_RE.concat(this.env.noParse), + exclude: BABEL_EXCLUDE_RE.concat(this.uiBundles.getWebpackNoParseRules()), use: [ { loader: 'cache-loader', @@ -187,12 +186,12 @@ export default class BaseOptimizer { }, ], }, - ...this.env.postLoaders.map(loader => ({ + ...this.uiBundles.getPostLoaders().map(loader => ({ enforce: 'post', ...loader })), ], - noParse: this.env.noParse, + noParse: this.uiBundles.getWebpackNoParseRules(), }, resolve: { @@ -205,12 +204,12 @@ export default class BaseOptimizer { 'node_modules', fromRoot('node_modules'), ], - alias: this.env.aliases, + alias: this.uiBundles.getAliases(), unsafeCache: this.unsafeCache, }, }; - if (this.env.context.env === 'development') { + if (this.uiBundles.isDevMode()) { return webpackMerge(commonConfig, { // In the test env we need to add react-addons (and a few other bits) for the // enzyme tests to work. diff --git a/src/optimize/index.js b/src/optimize/index.js index 50caaadc7bf70c..dde11b0ef3060a 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -17,19 +17,21 @@ export default async (kbnServer, server, config) => { return await kbnServer.mixin(require('./lazy/lazy')); } - const bundles = kbnServer.bundles; + const { uiBundles } = kbnServer; server.route(createBundlesRoute({ - bundlesPath: bundles.env.workingDir, + bundlesPath: uiBundles.getWorkingDir(), basePublicPath: config.get('server.basePath') })); - await bundles.writeEntryFiles(); + await uiBundles.writeEntryFiles(); // in prod, only bundle when someing is missing or invalid - const invalidBundles = config.get('optimize.useBundleCache') ? await bundles.getInvalidBundles() : bundles; + const reuseCache = config.get('optimize.useBundleCache') + ? await uiBundles.areAllBundleCachesValid() + : false; // we might not have any work to do - if (!invalidBundles.getIds().length) { + if (reuseCache) { server.log( ['debug', 'optimize'], `All bundles are cached and ready to go!` @@ -39,8 +41,7 @@ export default async (kbnServer, server, config) => { // only require the FsOptimizer when we need to const optimizer = new FsOptimizer({ - env: bundles.env, - bundles: bundles, + uiBundles, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), unsafeCache: config.get('optimize.unsafeCache'), @@ -48,12 +49,12 @@ export default async (kbnServer, server, config) => { server.log( ['info', 'optimize'], - `Optimizing and caching ${bundles.desc()}. This may take a few minutes` + `Optimizing and caching ${uiBundles.getDescription()}. This may take a few minutes` ); const start = Date.now(); await optimizer.run(); const seconds = ((Date.now() - start) / 1000).toFixed(2); - server.log(['info', 'optimize'], `Optimization of ${bundles.desc()} complete in ${seconds} seconds`); + server.log(['info', 'optimize'], `Optimization of ${uiBundles.getDescription()} complete in ${seconds} seconds`); }; diff --git a/src/optimize/lazy/lazy_optimizer.js b/src/optimize/lazy/lazy_optimizer.js index 36db7adc2c50ea..82d4b88fb1436a 100644 --- a/src/optimize/lazy/lazy_optimizer.js +++ b/src/optimize/lazy/lazy_optimizer.js @@ -23,7 +23,7 @@ export default class LazyOptimizer extends BaseOptimizer { async init() { this.initializing = true; - await this.bundles.writeEntryFiles(); + await this.uiBundles.writeEntryFiles(); await this.initCompiler(); this.compiler.plugin('watch-run', (w, webpackCb) => { @@ -59,8 +59,8 @@ export default class LazyOptimizer extends BaseOptimizer { this.initializing = false; this.log(['info', 'optimize'], { - tmpl: `Lazy optimization of ${this.bundles.desc()} ready`, - bundles: this.bundles.getIds() + tmpl: `Lazy optimization of ${this.uiBundles.getDescription()} ready`, + bundles: this.uiBundles.getIds() }); } @@ -90,14 +90,14 @@ export default class LazyOptimizer extends BaseOptimizer { logRunStart() { this.log(['info', 'optimize'], { tmpl: `Lazy optimization started`, - bundles: this.bundles.getIds() + bundles: this.uiBundles.getIds() }); } logRunSuccess() { this.log(['info', 'optimize'], { tmpl: 'Lazy optimization <%= status %> in <%= seconds %> seconds', - bundles: this.bundles.getIds(), + bundles: this.uiBundles.getIds(), status: 'success', seconds: this.timer.end() }); @@ -110,7 +110,7 @@ export default class LazyOptimizer extends BaseOptimizer { this.log(['fatal', 'optimize'], { tmpl: 'Lazy optimization <%= status %> in <%= seconds %> seconds<%= err %>', - bundles: this.bundles.getIds(), + bundles: this.uiBundles.getIds(), status: 'failed', seconds: this.timer.end(), err: err diff --git a/src/optimize/lazy/optmzr_role.js b/src/optimize/lazy/optmzr_role.js index 284b0cc32317d4..ba50258078e76d 100644 --- a/src/optimize/lazy/optmzr_role.js +++ b/src/optimize/lazy/optmzr_role.js @@ -8,8 +8,7 @@ export default async (kbnServer, kibanaHapiServer, config) => { config.get('server.basePath'), new LazyOptimizer({ log: (tags, data) => kibanaHapiServer.log(tags, data), - env: kbnServer.bundles.env, - bundles: kbnServer.bundles, + uiBundles: kbnServer.uiBundles, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), prebuild: config.get('optimize.lazyPrebuild'), diff --git a/src/plugin_discovery/index.js b/src/plugin_discovery/index.js index bc5b4a1d74f276..bfee8021766a64 100644 --- a/src/plugin_discovery/index.js +++ b/src/plugin_discovery/index.js @@ -1,2 +1,3 @@ export { findPluginSpecs } from './find_plugin_specs'; -export { collectUiExports } from './collect_ui_exports'; +export { reduceExportSpecs } from './plugin_exports'; +export { PluginPack } from './plugin_pack'; diff --git a/src/plugin_discovery/ui_export_types/webpack_customizations.js b/src/plugin_discovery/ui_export_types/webpack_customizations.js deleted file mode 100644 index 0939d71eb054f7..00000000000000 --- a/src/plugin_discovery/ui_export_types/webpack_customizations.js +++ /dev/null @@ -1,5 +0,0 @@ -import { concat, merge } from './reduce'; -import { alias, wrap, uniqueKeys } from './modify_reduce'; - -export const noParse = wrap(alias('webpackNoParseRules'), concat); -export const __globalImportAliases__ = wrap(alias('webpackAliases'), uniqueKeys('__globalImportAliases__'), merge); diff --git a/src/server/http/index.js b/src/server/http/index.js index 50466d884c137f..41ddd4fe719b62 100644 --- a/src/server/http/index.js +++ b/src/server/http/index.js @@ -138,7 +138,7 @@ export default async function (kbnServer, server, config) { return; } - const app = kbnServer.uiExports.apps.byId.stateSessionStorageRedirect; + const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); reply.renderApp(app, { redirectUrl: url, }); diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 8ed9760701c653..98aa55a55a88e4 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -1,28 +1,24 @@ import { constant, once, compact, flatten } from 'lodash'; -import { resolve, fromNode } from 'bluebird'; +import { fromNode } from 'bluebird'; import { isWorker } from 'cluster'; import { fromRoot, pkg } from '../utils'; -import Config from './config/config'; +import { Config } from './config'; import loggingConfiguration from './logging/configuration'; - import configSetupMixin from './config/setup'; import httpMixin from './http'; import loggingMixin from './logging'; import warningsMixin from './warnings'; import statusMixin from './status'; import pidMixin from './pid'; -import pluginsScanMixin from './plugins/scan'; -import pluginsCheckEnabledMixin from './plugins/check_enabled'; -import pluginsCheckVersionMixin from './plugins/check_version'; import configCompleteMixin from './config/complete'; -import uiMixin from '../ui'; import optimizeMixin from '../optimize'; -import pluginsInitializeMixin from './plugins/initialize'; +import * as Plugins from './plugins'; import { indexPatternsMixin } from './index_patterns'; import { savedObjectsMixin } from './saved_objects'; import { statsMixin } from './stats'; import { kibanaIndexMappingsMixin } from './mappings'; import { serverExtensionsMixin } from './server_extensions'; +import { uiMixin } from '../ui'; const rootDir = fromRoot('.'); @@ -35,6 +31,8 @@ export default class KbnServer { this.settings = settings || {}; this.ready = constant(this.mixin( + Plugins.waitForInitSetupMixin, + // sets this.config, reads this.settings configSetupMixin, // sets this.server @@ -52,13 +50,10 @@ export default class KbnServer { pidMixin, // find plugins and set this.plugins - pluginsScanMixin, - - // disable the plugins that are disabled through configuration - pluginsCheckEnabledMixin, + Plugins.scanMixin, // disable the plugins that are incompatible with the current version of Kibana - pluginsCheckVersionMixin, + Plugins.checkVersionsMixin, // tell the config we are done loading plugins configCompleteMixin, @@ -66,7 +61,7 @@ export default class KbnServer { // setup kbnServer.mappings and server.getKibanaIndexMappingsDsl() kibanaIndexMappingsMixin, - // setup this.uiExports and this.bundles + // setup this.uiExports and this.uiBundles uiMixin, indexPatternsMixin, @@ -77,11 +72,15 @@ export default class KbnServer { // lazy bundle server is running optimizeMixin, - // finally, initialize the plugins - pluginsInitializeMixin, + // initialize the plugins + Plugins.initializeMixin, + + // notify any deffered setup logic that plugins have intialized + Plugins.waitForInitResolveMixin, + () => { if (this.config.get('server.autoListen')) { - this.ready = constant(resolve()); + this.ready = constant(Promise.resolve()); return this.listen(); } } @@ -134,17 +133,11 @@ export default class KbnServer { } async inject(opts) { - if (!this.server) await this.ready(); - - return await fromNode(cb => { - try { - this.server.inject(opts, (resp) => { - cb(null, resp); - }); - } catch (err) { - cb(err); - } - }); + if (!this.server) { + await this.ready(); + } + + return await this.server.inject(opts); } applyLoggingConfiguration(settings) { diff --git a/src/server/plugins/check_enabled.js b/src/server/plugins/check_enabled.js deleted file mode 100644 index 70f163527193f0..00000000000000 --- a/src/server/plugins/check_enabled.js +++ /dev/null @@ -1,25 +0,0 @@ -import toPath from 'lodash/internal/toPath'; - -export default async function (kbnServer, server, config) { - const forcedOverride = { - console: function (enabledInConfig) { - return !config.get('elasticsearch.tribe.url') && enabledInConfig; - } - }; - - const { plugins } = kbnServer; - - for (const plugin of plugins) { - const enabledInConfig = config.get([...toPath(plugin.configPrefix), 'enabled']); - const hasOveride = forcedOverride.hasOwnProperty(plugin.id); - if (hasOveride) { - if (!forcedOverride[plugin.id](enabledInConfig)) { - plugins.disable(plugin); - } - } else if (!enabledInConfig) { - plugins.disable(plugin); - } - } - - return; -}; diff --git a/src/server/plugins/check_version.js b/src/server/plugins/check_version.js deleted file mode 100644 index 46199270f27399..00000000000000 --- a/src/server/plugins/check_version.js +++ /dev/null @@ -1,36 +0,0 @@ -import { cleanVersion, versionSatisfies } from '../../utils/version'; -import { get } from 'lodash'; - -function compatibleWithKibana(kbnServer, plugin) { - //core plugins have a version of 'kibana' and are always compatible - if (plugin.kibanaVersion === 'kibana') return true; - - const pluginKibanaVersion = cleanVersion(plugin.kibanaVersion); - const kibanaVersion = cleanVersion(kbnServer.version); - - return versionSatisfies(pluginKibanaVersion, kibanaVersion); -} - -export default async function (kbnServer, server) { - //because a plugin pack can contain more than one actual plugin, (for example x-pack) - //we make sure that the warning messages are unique - const warningMessages = new Set(); - const plugins = kbnServer.plugins; - - for (const plugin of plugins) { - const version = plugin.kibanaVersion; - const name = get(plugin, 'pkg.name'); - - if (!compatibleWithKibana(kbnServer, plugin)) { - const message = `Plugin "${name}" was disabled because it expected Kibana version "${version}", and found "${kbnServer.version}".`; - warningMessages.add(message); - plugins.disable(plugin); - } - } - - for (const message of warningMessages) { - server.log(['warning'], message); - } - - return; -}; diff --git a/src/server/plugins/check_versions_mixin.js b/src/server/plugins/check_versions_mixin.js new file mode 100644 index 00000000000000..2f5e1192ce4ecb --- /dev/null +++ b/src/server/plugins/check_versions_mixin.js @@ -0,0 +1,32 @@ +import { isVersionCompatible } from './lib'; + +/** + * Check that plugin versions match Kibana version, otherwise + * disable them + * + * @param {KbnServer} kbnServer + * @param {Hapi.Server} server + * @return {Promise} + */ +export function checkVersionsMixin(kbnServer, server) { + // because a plugin pack can contain more than one actual plugin, (for example x-pack) + // we make sure that the warning messages are unique + const warningMessages = new Set(); + const plugins = kbnServer.plugins; + const kibanaVersion = kbnServer.version; + + for (const plugin of plugins) { + const name = plugin.id; + const pluginVersion = plugin.kibanaVersion; + + if (!isVersionCompatible(pluginVersion, kibanaVersion)) { + const message = `Plugin "${name}" was disabled because it expected Kibana version "${pluginVersion}", and found "${kibanaVersion}".`; + warningMessages.add(message); + plugins.disable(plugin); + } + } + + for (const message of warningMessages) { + server.log(['warning'], message); + } +} diff --git a/src/server/plugins/index.js b/src/server/plugins/index.js new file mode 100644 index 00000000000000..fc6820373d1d5d --- /dev/null +++ b/src/server/plugins/index.js @@ -0,0 +1,4 @@ +export { scanMixin } from './scan_mixin'; +export { checkVersionsMixin } from './check_versions_mixin'; +export { initializeMixin } from './initialize_mixin'; +export { waitForInitSetupMixin, waitForInitResolveMixin } from './wait_for_plugins_init'; diff --git a/src/server/plugins/initialize.js b/src/server/plugins/initialize.js deleted file mode 100644 index 34d6c263b66716..00000000000000 --- a/src/server/plugins/initialize.js +++ /dev/null @@ -1,22 +0,0 @@ -import pluginInit from './plugin_init'; - -export default async function (kbnServer, server, config) { - - if (!config.get('plugins.initialize')) { - server.log(['info'], 'Plugin initialization disabled.'); - return []; - } - - const { plugins } = kbnServer; - - // extend plugin apis with additional context - plugins.getPluginApis().forEach(api => { - - Object.defineProperty(api, 'uiExports', { - value: kbnServer.uiExports - }); - - }); - - await pluginInit(plugins); -}; diff --git a/src/server/plugins/initialize_mixin.js b/src/server/plugins/initialize_mixin.js new file mode 100644 index 00000000000000..bd7a30565e719f --- /dev/null +++ b/src/server/plugins/initialize_mixin.js @@ -0,0 +1,27 @@ +import { callPluginHook } from './lib'; + +/** + * KbnServer mixin that initializes all plugins found in ./scan mixin + * @param {KbnServer} kbnServer + * @param {Hapi.Server} server + * @param {Config} config + * @return {Promise} + */ +export async function initializeMixin(kbnServer, server, config) { + if (!config.get('plugins.initialize')) { + server.log(['info'], 'Plugin initialization disabled.'); + return; + } + + async function callHookOnPlugins(hookName) { + const { plugins } = kbnServer; + const ids = plugins.map(p => p.id); + + for (const id of ids) { + await callPluginHook(hookName, plugins, id, []); + } + } + + await callHookOnPlugins('preInit'); + await callHookOnPlugins('init'); +} diff --git a/src/server/plugins/lib/call_plugin_hook.js b/src/server/plugins/lib/call_plugin_hook.js new file mode 100644 index 00000000000000..3ba3bdb846ee60 --- /dev/null +++ b/src/server/plugins/lib/call_plugin_hook.js @@ -0,0 +1,31 @@ +import { last } from 'lodash'; + +export async function callPluginHook(hookName, plugins, id, history) { + const plugin = plugins.find(plugin => plugin.id === id); + + // make sure this is a valid plugin id + if (!plugin) { + if (history.length) { + throw new Error(`Unmet requirement "${id}" for plugin "${last(history)}"`); + } else { + throw new Error(`Unknown plugin "${id}"`); + } + } + + const circleStart = history.indexOf(id); + const path = [...history, id]; + + // make sure we are not trying to load a dependency within itself + if (circleStart > -1) { + const circle = path.slice(circleStart); + throw new Error(`circular dependency found: "${circle.join(' -> ')}"`); + } + + // call hook on all dependencies + for (const req of plugin.requiredIds) { + await callPluginHook(hookName, plugins, req, path); + } + + // call hook on this plugin + await plugin[hookName](); +} diff --git a/src/server/plugins/lib/index.js b/src/server/plugins/lib/index.js new file mode 100644 index 00000000000000..ec0d4d0bc67d7a --- /dev/null +++ b/src/server/plugins/lib/index.js @@ -0,0 +1,3 @@ +export { callPluginHook } from './call_plugin_hook'; +export { isVersionCompatible } from './is_version_compatible'; +export { Plugin } from './plugin'; diff --git a/src/server/plugins/lib/is_version_compatible.js b/src/server/plugins/lib/is_version_compatible.js new file mode 100644 index 00000000000000..d120b1a21951f9 --- /dev/null +++ b/src/server/plugins/lib/is_version_compatible.js @@ -0,0 +1,17 @@ +import { + cleanVersion, + versionSatisfies +} from '../../../utils/version'; + +export function isVersionCompatible(version, compatibleWith) { + // the special "kibana" version can be used to always be compatible, + // but is intentionally not supported by the plugin installer + if (version === 'kibana') { + return true; + } + + return versionSatisfies( + cleanVersion(version), + cleanVersion(compatibleWith) + ); +} diff --git a/src/server/plugins/lib/plugin.js b/src/server/plugins/lib/plugin.js new file mode 100644 index 00000000000000..8fb3b387324b4e --- /dev/null +++ b/src/server/plugins/lib/plugin.js @@ -0,0 +1,106 @@ +import { once, noop } from 'lodash'; +import Bluebird, { attempt, fromNode } from 'bluebird'; + +/** + * The server plugin class, used to extend the server + * and add custom behavior. A "scoped" plugin class is + * created by the PluginApi class and provided to plugin + * providers that automatically binds all but the `opts` + * arguments. + * + * @class Plugin + * @param {KbnServer} kbnServer - the KbnServer this plugin + * belongs to. + * @param {PluginDefinition} def + * @param {PluginSpec} spec + */ +export class Plugin { + constructor(kbnServer, spec) { + this.kbnServer = kbnServer; + this.spec = spec; + this.pkg = spec.getPkg(); + this.path = spec.getPath(); + this.id = spec.getId(); + this.version = spec.getVersion(); + this.requiredIds = spec.getRequiredPluginIds(); + this.uiExportsSpecs = spec.getExportSpecs(); + this.kibanaVersion = spec.getExpectedKibanaVersion(); + this.externalPreInit = spec.getPreInitHandler(); + this.externalInit = spec.getInitHandler(); + this.enabled = spec.isEnabled(kbnServer.config); + this.configPrefix = spec.getConfigPrefix(); + this.publicDir = spec.getPublicDir(); + + this.preInit = once(this.preInit); + this.init = once(this.init); + } + + async preInit() { + return await this.externalPreInit(this.kbnServer.server); + } + + async init() { + const { id, version, kbnServer, configPrefix } = this; + const { config } = kbnServer; + + // setup the hapi register function and get on with it + const asyncRegister = async (server, options) => { + this._server = server; + this._options = options; + + server.log(['plugins', 'debug'], { + tmpl: 'Initializing plugin <%= plugin.toString() %>', + plugin: this + }); + + if (this.publicDir) { + server.exposeStaticDir(`/plugins/${id}/{path*}`, this.publicDir); + } + + // Many of the plugins are simply adding static assets to the server and we don't need + // to track their "status". Since plugins must have an init() function to even set its status + // we shouldn't even create a status unless the plugin can use it. + if (this.externalInit !== noop) { + this.status = kbnServer.status.createForPlugin(this); + server.expose('status', this.status); + } + + return await attempt(this.externalInit, [server, options], this); + }; + + const register = (server, options, next) => { + Bluebird.resolve(asyncRegister(server, options)).nodeify(next); + }; + + register.attributes = { name: id, version: version }; + + await fromNode(cb => { + kbnServer.server.register({ + register: register, + options: config.has(configPrefix) ? config.get(configPrefix) : null + }, cb); + }); + + // Only change the plugin status to green if the + // intial status has not been changed + if (this.status && this.status.state === 'uninitialized') { + this.status.green('Ready'); + } + } + + getServer() { + return this._server; + } + + getOptions() { + return this._options; + } + + toJSON() { + return this.pkg; + } + + toString() { + return `${this.id}@${this.version}`; + } +} diff --git a/src/server/plugins/plugin.js b/src/server/plugins/plugin.js deleted file mode 100644 index f2ffcff0ee354e..00000000000000 --- a/src/server/plugins/plugin.js +++ /dev/null @@ -1,175 +0,0 @@ -import _ from 'lodash'; -import Joi from 'joi'; -import Bluebird, { attempt, fromNode } from 'bluebird'; -import { basename, resolve } from 'path'; -import { Deprecations } from '../../deprecation'; - -const extendInitFns = Symbol('extend plugin initialization'); - -const defaultConfigSchema = Joi.object({ - enabled: Joi.boolean().default(true) -}).default(); - -/** - * The server plugin class, used to extend the server - * and add custom behavior. A "scoped" plugin class is - * created by the PluginApi class and provided to plugin - * providers that automatically binds all but the `opts` - * arguments. - * - * @class Plugin - * @param {KbnServer} kbnServer - the KbnServer this plugin - * belongs to. - * @param {String} path - the path from which the plugin hails - * @param {Object} pkg - the value of package.json for the plugin - * @param {Objects} opts - the options for this plugin - * @param {String} [opts.id=pkg.name] - the id for this plugin. - * @param {Object} [opts.uiExports] - a mapping of UiExport types - * to UI modules or metadata about - * the UI module - * @param {Array} [opts.require] - the other plugins that this plugin - * requires. These plugins must exist and - * be enabled for this plugin to function. - * The require'd plugins will also be - * initialized first, in order to make sure - * that dependencies provided by these plugins - * are available - * @param {String} [opts.version=pkg.version] - the version of this plugin - * @param {Function} [opts.init] - A function that will be called to initialize - * this plugin at the appropriate time. - * @param {Function} [opts.configPrefix=this.id] - The prefix to use for configuration - * values in the main configuration service - * @param {Function} [opts.config] - A function that produces a configuration - * schema using Joi, which is passed as its - * first argument. - * @param {String|False} [opts.publicDir=path + '/public'] - * - the public directory for this plugin. The final directory must - * have the name "public", though it can be located somewhere besides - * the root of the plugin. Set this to false to disable exposure of a - * public directory - */ -export default class Plugin { - constructor(kbnServer, path, pkg, opts) { - this.kbnServer = kbnServer; - this.pkg = pkg; - this.path = path; - - this.id = opts.id || pkg.name; - this.uiExportsSpecs = opts.uiExports || {}; - this.requiredIds = opts.require || []; - this.version = opts.version || pkg.version; - - // Plugins must specify their version, and by default that version should match - // the version of kibana down to the patch level. If these two versions need - // to diverge, they can specify a kibana.version in the package to indicate the - // version of kibana the plugin is intended to work with. - this.kibanaVersion = opts.kibanaVersion || _.get(pkg, 'kibana.version', this.version); - this.externalPreInit = opts.preInit || _.noop; - this.externalInit = opts.init || _.noop; - this.configPrefix = opts.configPrefix || this.id; - this.getExternalConfigSchema = opts.config || _.noop; - this.getExternalDeprecations = opts.deprecations || _.noop; - this.preInit = _.once(this.preInit); - this.init = _.once(this.init); - this[extendInitFns] = []; - - if (opts.publicDir === false) { - this.publicDir = null; - } - else if (!opts.publicDir) { - this.publicDir = resolve(this.path, 'public'); - } - else { - this.publicDir = opts.publicDir; - if (basename(this.publicDir) !== 'public') { - throw new Error(`publicDir for plugin ${this.id} must end with a "public" directory.`); - } - } - } - - static scoped(kbnServer, path, pkg) { - return class ScopedPlugin extends Plugin { - constructor(opts) { - super(kbnServer, path, pkg, opts || {}); - } - }; - } - - async getConfigSchema() { - const schema = await this.getExternalConfigSchema(Joi); - return schema || defaultConfigSchema; - } - - getDeprecations() { - const rules = this.getExternalDeprecations(Deprecations); - return rules || []; - } - - async preInit() { - return await this.externalPreInit(this.kbnServer.server); - } - - async init() { - const { id, version, kbnServer, configPrefix } = this; - const { config } = kbnServer; - - // setup the hapi register function and get on with it - const asyncRegister = async (server, options) => { - this.server = server; - - for (const fn of this[extendInitFns]) { - await fn.call(this, server, options); - } - - server.log(['plugins', 'debug'], { - tmpl: 'Initializing plugin <%= plugin.toString() %>', - plugin: this - }); - - if (this.publicDir) { - server.exposeStaticDir(`/plugins/${id}/{path*}`, this.publicDir); - } - - // Many of the plugins are simply adding static assets to the server and we don't need - // to track their "status". Since plugins must have an init() function to even set its status - // we shouldn't even create a status unless the plugin can use it. - if (this.externalInit !== _.noop) { - this.status = kbnServer.status.createForPlugin(this); - server.expose('status', this.status); - } - - return await attempt(this.externalInit, [server, options], this); - }; - - const register = (server, options, next) => { - Bluebird.resolve(asyncRegister(server, options)).nodeify(next); - }; - - register.attributes = { name: id, version: version }; - - await fromNode(cb => { - kbnServer.server.register({ - register: register, - options: config.has(configPrefix) ? config.get(configPrefix) : null - }, cb); - }); - - // Only change the plugin status to green if the - // intial status has not been changed - if (this.status && this.status.state === 'uninitialized') { - this.status.green('Ready'); - } - } - - extendInit(fn) { - this[extendInitFns].push(fn); - } - - toJSON() { - return this.pkg; - } - - toString() { - return `${this.id}@${this.version}`; - } -} diff --git a/src/server/plugins/plugin_api.js b/src/server/plugins/plugin_api.js deleted file mode 100644 index 53e747d775187a..00000000000000 --- a/src/server/plugins/plugin_api.js +++ /dev/null @@ -1,30 +0,0 @@ -import Plugin from './plugin'; -import { join } from 'path'; - -export default class PluginApi { - constructor(kibana, pluginPath) { - this.config = kibana.config; - this.rootDir = kibana.rootDir; - this.package = require(join(pluginPath, 'package.json')); - this.Plugin = Plugin.scoped(kibana, pluginPath, this.package); - } - - get uiExports() { - throw new Error('plugin.uiExports is not defined until initialize phase'); - } - - get autoload() { - console.warn( - `${this.package.id} accessed the autoload lists which are no longer available via the Plugin API.` + - 'Use the `ui/autoload/*` modules instead.' - ); - - return { - directives: [], - filters: [], - styles: [], - modules: [], - require: [] - }; - } -} diff --git a/src/server/plugins/plugin_collection.js b/src/server/plugins/plugin_collection.js deleted file mode 100644 index dd7d1afd4ead79..00000000000000 --- a/src/server/plugins/plugin_collection.js +++ /dev/null @@ -1,78 +0,0 @@ - -import PluginApi from './plugin_api'; -import { inspect } from 'util'; -import { get, indexBy } from 'lodash'; -import Collection from '../../utils/collection'; -import { transformDeprecations } from '../config/transform_deprecations'; -import { createTransform } from '../../deprecation'; -import Joi from 'joi'; - -const byIdCache = Symbol('byIdCache'); -const pluginApis = Symbol('pluginApis'); - -async function addPluginConfig(pluginCollection, plugin) { - const { config, server, settings } = pluginCollection.kbnServer; - - const transformedSettings = transformDeprecations(settings); - const pluginSettings = get(transformedSettings, plugin.configPrefix); - const deprecations = plugin.getDeprecations(); - const transformedPluginSettings = createTransform(deprecations)(pluginSettings, (message) => { - server.log(['warning', plugin.configPrefix, 'config', 'deprecation'], message); - }); - - const configSchema = await plugin.getConfigSchema(); - config.extendSchema(configSchema, transformedPluginSettings, plugin.configPrefix); -} - -function disablePluginConfig(pluginCollection, plugin) { - // when disabling a plugin's config we remove the existing schema and - // replace it with a simple schema/config that only has enabled set to false - const { config } = pluginCollection.kbnServer; - config.removeSchema(plugin.configPrefix); - const schema = Joi.object({ enabled: Joi.bool() }); - config.extendSchema(schema, { enabled: false }, plugin.configPrefix); -} - -export default class Plugins extends Collection { - - constructor(kbnServer) { - super(); - this.kbnServer = kbnServer; - this[pluginApis] = new Set(); - } - - async new(path) { - const api = new PluginApi(this.kbnServer, path); - this[pluginApis].add(api); - - const output = [].concat(require(path)(api) || []); - - if (!output.length) return; - - // clear the byIdCache - this[byIdCache] = null; - - for (const plugin of output) { - if (!plugin instanceof api.Plugin) { - throw new TypeError('unexpected plugin export ' + inspect(plugin)); - } - - await addPluginConfig(this, plugin); - this.add(plugin); - } - } - - async disable(plugin) { - disablePluginConfig(this, plugin); - this.delete(plugin); - } - - get byId() { - return this[byIdCache] || (this[byIdCache] = indexBy([...this], 'id')); - } - - getPluginApis() { - return this[pluginApis]; - } - -} diff --git a/src/server/plugins/plugin_init.js b/src/server/plugins/plugin_init.js deleted file mode 100644 index 9a04e9d8f4d7db..00000000000000 --- a/src/server/plugins/plugin_init.js +++ /dev/null @@ -1,35 +0,0 @@ -import { includes } from 'lodash'; - -export default async (plugins) => { - const path = []; - - const initialize = async function (id, fn) { - const plugin = plugins.byId[id]; - - if (includes(path, id)) { - throw new Error(`circular dependencies found: "${path.concat(id).join(' -> ')}"`); - } - - path.push(id); - - for (const reqId of plugin.requiredIds) { - if (!plugins.byId[reqId]) { - throw new Error(`Unmet requirement "${reqId}" for plugin "${id}"`); - } - - await initialize(reqId, fn); - } - - await plugin[fn](); - path.pop(); - }; - - const collection = plugins.toArray(); - for (const { id } of collection) { - await initialize(id, 'preInit'); - } - - for (const { id } of collection) { - await initialize(id, 'init'); - } -}; diff --git a/src/server/plugins/scan.js b/src/server/plugins/scan.js deleted file mode 100644 index 41e406015c70a0..00000000000000 --- a/src/server/plugins/scan.js +++ /dev/null @@ -1,59 +0,0 @@ -import _ from 'lodash'; -import { fromNode, each } from 'bluebird'; -import { readdir, stat } from 'fs'; -import { resolve } from 'path'; -import PluginCollection from './plugin_collection'; - -export default async (kbnServer, server, config) => { - - const plugins = kbnServer.plugins = new PluginCollection(kbnServer); - - const scanDirs = [].concat(config.get('plugins.scanDirs') || []); - const pluginPaths = [].concat(config.get('plugins.paths') || []); - - const debug = _.bindKey(server, 'log', ['plugins', 'debug']); - const warning = _.bindKey(server, 'log', ['plugins', 'warning']); - - // scan all scanDirs to find pluginPaths - await each(scanDirs, async dir => { - debug({ tmpl: 'Scanning `<%= dir %>` for plugins', dir: dir }); - - let filenames = null; - - try { - filenames = await fromNode(cb => readdir(dir, cb)); - } catch (err) { - if (err.code !== 'ENOENT') throw err; - - filenames = []; - warning({ - tmpl: '<%= err.code %>: Unable to scan non-existent directory for plugins "<%= dir %>"', - err: err, - dir: dir - }); - } - - await each(filenames, async name => { - if (name[0] === '.') return; - - const path = resolve(dir, name); - const stats = await fromNode(cb => stat(path, cb)); - if (stats.isDirectory()) { - pluginPaths.push(path); - } - }); - }); - - for (const path of pluginPaths) { - let modulePath; - try { - modulePath = require.resolve(path); - } catch (e) { - warning({ tmpl: 'Skipping non-plugin directory at <%= path %>', path: path }); - continue; - } - - await plugins.new(path); - debug({ tmpl: 'Found plugin at <%= path %>', path: modulePath }); - } -}; diff --git a/src/server/plugins/scan_mixin.js b/src/server/plugins/scan_mixin.js new file mode 100644 index 00000000000000..bda5db6f12b12b --- /dev/null +++ b/src/server/plugins/scan_mixin.js @@ -0,0 +1,51 @@ +import { Observable } from 'rxjs'; +import { findPluginSpecs } from '../../plugin_discovery'; + +import { Plugin } from './lib'; + +export async function scanMixin(kbnServer, server, config) { + const { + pack$, + invalidDirectoryError$, + invalidPackError$, + deprecation$, + spec$, + } = findPluginSpecs(kbnServer.settings, config); + + const logging$ = Observable.merge( + pack$.do(definition => { + server.log(['plugin', 'debug'], { + tmpl: 'Found plugin at <%= path %>', + path: definition.getPath() + }); + }), + + invalidDirectoryError$.do(error => { + server.log(['plugin', 'warning'], { + tmpl: '<%= err.code %>: Unable to scan directory for plugins "<%= dir %>"', + err: error, + dir: error.path + }); + }), + + invalidPackError$.do(error => { + server.log(['plugin', 'warning'], { + tmpl: 'Skipping non-plugin directory at <%= path %>', + path: error.path + }); + }), + + deprecation$.do(({ spec, message }) => { + server.log(['warning', spec.getConfigPrefix(), 'config', 'deprecation'], message); + }) + ); + + kbnServer.pluginSpecs = await spec$ + .merge(logging$.ignoreElements()) + .toArray() + .toPromise(); + + kbnServer.plugins = kbnServer.pluginSpecs.map(spec => ( + new Plugin(kbnServer, spec) + )); +} diff --git a/src/server/plugins/wait_for_plugins_init.js b/src/server/plugins/wait_for_plugins_init.js new file mode 100644 index 00000000000000..712f2b9d767ff6 --- /dev/null +++ b/src/server/plugins/wait_for_plugins_init.js @@ -0,0 +1,28 @@ + +const queues = new WeakMap(); + +export function waitForInitSetupMixin(kbnServer) { + queues.set(kbnServer, []); + + kbnServer.afterPluginsInit = function (callback) { + const queue = queues.get(kbnServer); + + if (!queue) { + throw new Error('Plugins have already initialized. Only use this method for setup logic that must wait for plugins to initialize.'); + } + + queue.push(callback); + }; +} + +export async function waitForInitResolveMixin(kbnServer, server, config) { + const queue = queues.get(kbnServer); + queues.set(kbnServer, null); + + // only actually call the callbacks if we are really initializing + if (config.get('plugins.initialize')) { + for (const cb of queue) { + await cb(); + } + } +} diff --git a/src/server/status/index.js b/src/server/status/index.js index e0c78bebf966f8..69f61cfeef0eb4 100644 --- a/src/server/status/index.js +++ b/src/server/status/index.js @@ -40,24 +40,23 @@ export default function (kbnServer, server, config) { })); server.decorate('reply', 'renderStatusPage', async function () { - const app = kbnServer.uiExports.getHiddenApp('status_page'); - const response = await getResponse(this); - response.code(kbnServer.status.isGreen() ? 200 : 503); - return response; - - function getResponse(ctx) { - if (app) { - return ctx.renderApp(app); - } - return ctx(kbnServer.status.toString()); + const app = server.getHiddenUiAppById('status_page'); + const reply = this; + const response = app + ? await reply.renderApp(app) + : reply(kbnServer.status.toString()); + + if (response) { + response.code(kbnServer.status.isGreen() ? 200 : 503); + return response; } }); server.route(wrapAuth({ method: 'GET', path: '/status', - handler: function (request, reply) { - return reply.renderStatusPage(); + handler(request, reply) { + reply.renderStatusPage(); } })); } diff --git a/src/ui/app_entry_template.js b/src/ui/app_entry_template.js deleted file mode 100644 index 2cd55ffdb4d898..00000000000000 --- a/src/ui/app_entry_template.js +++ /dev/null @@ -1,29 +0,0 @@ -export default function ({ env, bundle }) { - - const pluginSlug = env.pluginInfo.sort() - .map(p => ' * - ' + p) - .join('\n'); - - const requires = bundle.modules - .map(m => `require('${m}');`) - .join('\n'); - - return ` -/** - * Test entry file - * - * This is programatically created and updated, do not modify - * - * context: ${JSON.stringify(env.context)} - * includes code from: -${pluginSlug} - * - */ - -require('ui/chrome'); -${requires} -require('ui/chrome').bootstrap(/* xoxo */); - -`; - -} diff --git a/src/ui/field_formats_mixin.js b/src/ui/field_formats/field_formats_mixin.js similarity index 93% rename from src/ui/field_formats_mixin.js rename to src/ui/field_formats/field_formats_mixin.js index 4f151a537bb387..b3ae44928b2f25 100644 --- a/src/ui/field_formats_mixin.js +++ b/src/ui/field_formats/field_formats_mixin.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { FieldFormatsService } from './field_formats/field_formats_service'; +import { FieldFormatsService } from './field_formats_service'; export function fieldFormatsMixin(kbnServer, server) { const fieldFormatClasses = []; diff --git a/src/ui/field_formats/index.js b/src/ui/field_formats/index.js new file mode 100644 index 00000000000000..0ada23204bafa4 --- /dev/null +++ b/src/ui/field_formats/index.js @@ -0,0 +1,2 @@ +export { fieldFormatsMixin } from './field_formats_mixin'; +export { FieldFormat } from './field_format'; diff --git a/src/ui/index.js b/src/ui/index.js index 351913cc0747b1..8982c8fac03caa 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -1,137 +1,2 @@ -import { defaults, _ } from 'lodash'; -import { props, reduce as reduceAsync } from 'bluebird'; -import Boom from 'boom'; -import { resolve } from 'path'; - -import UiExports from './ui_exports'; -import UiBundle from './ui_bundle'; -import UiBundleCollection from './ui_bundle_collection'; -import UiBundlerEnv from './ui_bundler_env'; -import { UiI18n } from './ui_i18n'; - -import { uiSettingsMixin } from './ui_settings'; -import { fieldFormatsMixin } from './field_formats_mixin'; - -export default async (kbnServer, server, config) => { - const uiExports = kbnServer.uiExports = new UiExports({ - urlBasePath: config.get('server.basePath'), - kibanaIndexMappings: kbnServer.mappings, - }); - - await kbnServer.mixin(uiSettingsMixin); - - await kbnServer.mixin(fieldFormatsMixin); - - const uiI18n = kbnServer.uiI18n = new UiI18n(config.get('i18n.defaultLocale')); - uiI18n.addUiExportConsumer(uiExports); - - const bundlerEnv = new UiBundlerEnv(config.get('optimize.bundleDir')); - bundlerEnv.addContext('env', config.get('env.name')); - bundlerEnv.addContext('sourceMaps', config.get('optimize.sourceMaps')); - bundlerEnv.addContext('kbnVersion', config.get('pkg.version')); - bundlerEnv.addContext('buildNum', config.get('pkg.buildNum')); - uiExports.addConsumer(bundlerEnv); - - for (const plugin of kbnServer.plugins) { - uiExports.consumePlugin(plugin); - } - - const bundles = kbnServer.bundles = new UiBundleCollection(bundlerEnv, config.get('optimize.bundleFilter')); - - for (const app of uiExports.getAllApps()) { - bundles.addApp(app); - } - - for (const gen of uiExports.getBundleProviders()) { - const bundle = await gen(UiBundle, bundlerEnv, uiExports.getAllApps(), kbnServer.plugins); - if (bundle) bundles.add(bundle); - } - - // render all views from the ui/views directory - server.setupViews(resolve(__dirname, 'views')); - - server.route({ - path: '/app/{id}', - method: 'GET', - async handler(req, reply) { - const id = req.params.id; - const app = uiExports.apps.byId[id]; - if (!app) return reply(Boom.notFound('Unknown app ' + id)); - - try { - if (kbnServer.status.isGreen()) { - await reply.renderApp(app); - } else { - await reply.renderStatusPage(); - } - } catch (err) { - reply(Boom.boomify(err)); - } - } - }); - - async function getKibanaPayload({ app, request, includeUserProvidedConfig, injectedVarsOverrides }) { - const uiSettings = request.getUiSettingsService(); - const translations = await uiI18n.getTranslationsForRequest(request); - - return { - app: app, - nav: uiExports.navLinks.inOrder, - version: kbnServer.version, - branch: config.get('pkg.branch'), - buildNum: config.get('pkg.buildNum'), - buildSha: config.get('pkg.buildSha'), - basePath: config.get('server.basePath'), - serverName: config.get('server.name'), - devMode: config.get('env.dev'), - translations: translations, - uiSettings: await props({ - defaults: uiSettings.getDefaults(), - user: includeUserProvidedConfig && uiSettings.getUserProvided() - }), - vars: await reduceAsync( - uiExports.injectedVarsReplacers, - async (acc, replacer) => await replacer(acc, request, server), - defaults(injectedVarsOverrides, await app.getInjectedVars() || {}, uiExports.defaultInjectedVars) - ), - }; - } - - async function renderApp({ app, reply, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) { - try { - const request = reply.request; - const translations = await uiI18n.getTranslationsForRequest(request); - - return reply.view(app.templateName, { - app, - kibanaPayload: await getKibanaPayload({ - app, - request, - includeUserProvidedConfig, - injectedVarsOverrides - }), - bundlePath: `${config.get('server.basePath')}/bundles`, - i18n: key => _.get(translations, key, ''), - }); - } catch (err) { - reply(err); - } - } - - server.decorate('reply', 'renderApp', function (app, injectedVarsOverrides) { - return renderApp({ - app, - reply: this, - includeUserProvidedConfig: true, - injectedVarsOverrides, - }); - }); - - server.decorate('reply', 'renderAppWithDefaultConfig', function (app) { - return renderApp({ - app, - reply: this, - includeUserProvidedConfig: false, - }); - }); -}; +export { uiMixin } from './ui_mixin'; +export { collectUiExports } from './ui_exports'; diff --git a/src/ui/public/angular-bootstrap/bindHtml/bindHtml.js b/src/ui/public/angular-bootstrap/bindHtml/bindHtml.js index cf635bc375a849..bafc7382686265 100755 --- a/src/ui/public/angular-bootstrap/bindHtml/bindHtml.js +++ b/src/ui/public/angular-bootstrap/bindHtml/bindHtml.js @@ -7,4 +7,4 @@ angular.module('ui.bootstrap.bindHtml', []) element.html(value || ''); }); }; - }); \ No newline at end of file + }); diff --git a/src/ui/public/assets/favicons/manifest.json b/src/ui/public/assets/favicons/manifest.json index 25126387919d58..17b3c4b2d9e527 100644 --- a/src/ui/public/assets/favicons/manifest.json +++ b/src/ui/public/assets/favicons/manifest.json @@ -15,4 +15,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/src/ui/public/filter_bar/filter_bar.js b/src/ui/public/filter_bar/filter_bar.js index c5b86d5e4f0fa9..fc9ee84bde0051 100644 --- a/src/ui/public/filter_bar/filter_bar.js +++ b/src/ui/public/filter_bar/filter_bar.js @@ -13,9 +13,6 @@ import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; import { compareFilters } from './lib/compare_filters'; import { uiModules } from 'ui/modules'; -export { disableFilter, enableFilter, toggleFilterDisabled } from './lib/disable_filter'; - - const module = uiModules.get('kibana'); module.directive('filterBar', function (Private, Promise, getAppState) { diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index c0f34c3c100cb3..a85c238ccee2fa 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -180,7 +180,13 @@ export class SavedObjectsClient { return resolveUrl(this._apiBaseUrl, formatUrl({ pathname: join(...path), - query: _.pick(query, value => value != null) + query: _.pick(query, value => { + if (Array.isArray(value)) { + return !!value.length; + } + + return value != null; + }) })); } diff --git a/src/ui/ui_app.js b/src/ui/ui_app.js deleted file mode 100644 index 26a5e335e54117..00000000000000 --- a/src/ui/ui_app.js +++ /dev/null @@ -1,69 +0,0 @@ -import { chain, get, noop, once, pick } from 'lodash'; - -export default class UiApp { - constructor(uiExports, spec) { - this.uiExports = uiExports; - this.spec = spec || {}; - - this.id = this.spec.id; - if (!this.id) { - throw new Error('Every app must specify it\'s id'); - } - - this.main = this.spec.main; - this.title = this.spec.title; - this.order = this.spec.order || 0; - this.description = this.spec.description; - this.icon = this.spec.icon; - this.hidden = !!this.spec.hidden; - this.linkToLastSubUrl = this.spec.linkToLastSubUrl; - this.listed = this.spec.listed == null ? !this.hidden : this.spec.listed; - this.templateName = this.spec.templateName || 'ui_app'; - - if (!this.hidden) { - // any non-hidden app has a url, so it gets a "navLink" - this.navLink = this.uiExports.navLinks.new({ - id: this.id, - title: this.title, - order: this.order, - description: this.description, - icon: this.icon, - url: this.spec.url || `/app/${this.id}`, - linkToLastSubUrl: this.linkToLastSubUrl - }); - - if (!this.listed) { - // unlisted apps remove their navLinks from the uiExports collection though - this.uiExports.navLinks.delete(this.navLink); - } - } - - if (this.spec.autoload) { - console.warn( - `"autoload" (used by ${this.id} app) is no longer a valid app configuration directive.` + - 'Use the \`ui/autoload/*\` modules instead.' - ); - } - - // once this resolves, no reason to run it again - this.getModules = once(this.getModules); - - // variables that are injected into the browser, must serialize to JSON - this.getInjectedVars = this.spec.injectVars || noop; - } - - getModules() { - return chain([ - this.uiExports.find(get(this, 'spec.uses', [])), - this.uiExports.find(['chromeNavControls', 'hacks']), - ]) - .flatten() - .uniq() - .unshift(this.main) - .value(); - } - - toJSON() { - return pick(this, ['id', 'title', 'description', 'icon', 'main', 'navLink', 'linkToLastSubUrl']); - } -} diff --git a/src/ui/ui_app_collection.js b/src/ui/ui_app_collection.js deleted file mode 100644 index 979570d92c69f0..00000000000000 --- a/src/ui/ui_app_collection.js +++ /dev/null @@ -1,45 +0,0 @@ -import _ from 'lodash'; -import UiApp from './ui_app'; -import Collection from '../utils/collection'; - -const byIdCache = Symbol('byId'); - -export default class UiAppCollection extends Collection { - - constructor(uiExports, parent) { - super(); - - this.uiExports = uiExports; - - if (!parent) { - this.claimedIds = []; - this.hidden = new UiAppCollection(uiExports, this); - } else { - this.claimedIds = parent.claimedIds; - } - - } - - new(spec) { - if (this.hidden && spec.hidden) { - return this.hidden.new(spec); - } - - const app = new UiApp(this.uiExports, spec); - - if (_.includes(this.claimedIds, app.id)) { - throw new Error('Unable to create two apps with the id ' + app.id + '.'); - } else { - this.claimedIds.push(app.id); - } - - this[byIdCache] = null; - this.add(app); - return app; - } - - get byId() { - return this[byIdCache] || (this[byIdCache] = _.indexBy([...this], 'id')); - } - -} diff --git a/src/ui/ui_apps/index.js b/src/ui/ui_apps/index.js new file mode 100644 index 00000000000000..d2914dc94e0527 --- /dev/null +++ b/src/ui/ui_apps/index.js @@ -0,0 +1 @@ +export { uiAppsMixin } from './ui_apps_mixin'; diff --git a/src/ui/ui_apps/ui_app.js b/src/ui/ui_apps/ui_app.js new file mode 100644 index 00000000000000..157ade46516fc6 --- /dev/null +++ b/src/ui/ui_apps/ui_app.js @@ -0,0 +1,126 @@ +import { noop } from 'lodash'; + +import { UiNavLink } from '../ui_nav_links'; + +export class UiApp { + constructor(kbnServer, spec) { + const { + pluginId, + id = pluginId, + main, + title, + order = 0, + description, + icon, + hidden, + linkToLastSubUrl, + listed = !hidden, + templateName = 'ui_app', + injectVars = noop, + url = `/app/${id}`, + uses = [] + } = spec; + + if (!id) { + throw new Error('Every app must specify an id'); + } + + if (spec.autoload) { + console.warn( + `"autoload" (used by ${id} app) is no longer a valid app configuration directive.` + + 'Use the \`ui/autoload/*\` modules instead.' + ); + } + + this._id = id; + this._main = main; + this._title = title; + this._order = order; + this._description = description; + this._icon = icon; + this._linkToLastSubUrl = linkToLastSubUrl; + this._hidden = hidden; + this._listed = !hidden && listed; + this._templateName = templateName; + this._url = url; + this._pluginId = pluginId; + + const plugin = kbnServer.plugins + .find(plugin => plugin.id === this._pluginId); + + this._injectVars = () => { + if (!injectVars) { + return; + } + + const server = plugin.getServer(); + const options = plugin.getOptions(); + return injectVars.call(plugin, server, options); + }; + + const { appExtensions = [] } = kbnServer.uiExports; + this._modules = [].concat( + this._main, + ...uses.map(type => appExtensions[type] || []), + appExtensions.chromeNavControls || [], + appExtensions.hacks || [] + ); + + if (!this.isHidden()) { + // unless an app is hidden it gets a navlink, but we only respond to `getNavLink()` + // if the app is also listed. This means that all apps in the kibanaPayload will + // have a navLink property since that list includes all normally accessible apps + this._navLink = new UiNavLink(kbnServer, { + id: this._id, + title: this._title, + order: this._order, + description: this._description, + icon: this._icon, + url: this._url, + linkToLastSubUrl: this._linkToLastSubUrl + }); + } + } + + getId() { + return this._id; + } + + getPluginId() { + return this._pluginId; + } + + getTemplateName() { + return this._templateName; + } + + isHidden() { + return !!this._hidden; + } + + getNavLink() { + if (this._listed) { + return this._navLink; + } + } + + getInjectedVars() { + return this._injectVars(); + } + + getModules() { + return this._modules; + } + + toJSON() { + return { + id: this._id, + title: this._title, + description: this._description, + icon: this._icon, + main: this._main, + navLink: this._navLink, + linkToLastSubUrl: this._linkToLastSubUrl + }; + } +} diff --git a/src/ui/ui_apps/ui_apps_mixin.js b/src/ui/ui_apps/ui_apps_mixin.js new file mode 100644 index 00000000000000..7f60727bd36e59 --- /dev/null +++ b/src/ui/ui_apps/ui_apps_mixin.js @@ -0,0 +1,38 @@ +import { memoize } from 'lodash'; + +import { UiApp } from './ui_app'; + +export function uiAppsMixin(kbnServer, server) { + + const { uiAppSpecs = [] } = kbnServer.uiExports; + const existingIds = new Set(); + + kbnServer.uiApps = uiAppSpecs.map(spec => { + const app = new UiApp(kbnServer, spec); + const id = app.getId(); + + if (!existingIds.has(id)) { + existingIds.add(id); + } else { + throw new Error(`Unable to create two apps with the id ${id}.`); + } + + return app; + }); + + server.decorate('server', 'getAllUiApps', () => ( + kbnServer.uiApps.slice(0) + )); + + server.decorate('server', 'getUiAppById', memoize(id => ( + kbnServer.uiApps.find(uiApp => ( + uiApp.getId() === id && !uiApp.isHidden() + )) + ))); + + server.decorate('server', 'getHiddenUiAppById', memoize(id => ( + kbnServer.uiApps.find(uiApp => ( + uiApp.getId() === id && uiApp.isHidden() + )) + ))); +} diff --git a/src/ui/ui_bundle.js b/src/ui/ui_bundle.js deleted file mode 100644 index 4d3f6e6f49d23a..00000000000000 --- a/src/ui/ui_bundle.js +++ /dev/null @@ -1,68 +0,0 @@ -import { join } from 'path'; -import { promisify } from 'bluebird'; - -const read = promisify(require('fs').readFile); -const write = promisify(require('fs').writeFile); -const unlink = promisify(require('fs').unlink); -const stat = promisify(require('fs').stat); - -export default class UiBundle { - constructor(opts) { - - opts = opts || {}; - this.id = opts.id; - this.modules = opts.modules; - this.template = opts.template; - this.env = opts.env; - - const pathBase = join(this.env.workingDir, this.id); - this.entryPath = `${pathBase}.entry.js`; - this.outputPath = `${pathBase}.bundle.js`; - - } - - renderContent() { - return this.template({ - env: this.env, - bundle: this - }); - } - - async readEntryFile() { - try { - const content = await read(this.entryPath); - return content.toString('utf8'); - } - catch (e) { - return null; - } - } - - async writeEntryFile() { - return await write(this.entryPath, this.renderContent(), { encoding: 'utf8' }); - } - - async clearBundleFile() { - try { await unlink(this.outputPath); } - catch (e) { return null; } - } - - async checkForExistingOutput() { - try { - await stat(this.outputPath); - return true; - } - catch (e) { - return false; - } - } - - toJSON() { - return { - id: this.id, - modules: this.modules, - entryPath: this.entryPath, - outputPath: this.outputPath - }; - } -} diff --git a/src/ui/ui_bundle_collection.js b/src/ui/ui_bundle_collection.js deleted file mode 100644 index b4ce0992e26177..00000000000000 --- a/src/ui/ui_bundle_collection.js +++ /dev/null @@ -1,108 +0,0 @@ -import { createHash } from 'crypto'; - -import UiBundle from './ui_bundle'; -import appEntryTemplate from './app_entry_template'; -import { transform, pluck } from 'lodash'; -import { promisify } from 'bluebird'; -import { makeRe } from 'minimatch'; - -const mkdirp = promisify(require('mkdirp')); - -export default class UiBundleCollection { - constructor(bundlerEnv, filter) { - this.each = []; - this.env = bundlerEnv; - this.filter = makeRe(filter || '*', { - noglobstar: true, - noext: true, - matchBase: true - }); - } - - add(bundle) { - if (!(bundle instanceof UiBundle)) { - throw new TypeError('expected bundle to be an instance of UiBundle'); - } - - if (this.filter.test(bundle.id)) { - this.each.push(bundle); - } - } - - addApp(app) { - this.add(new UiBundle({ - id: app.id, - modules: app.getModules(), - template: appEntryTemplate, - env: this.env - })); - } - - desc() { - switch (this.each.length) { - case 0: - return '0 bundles'; - case 1: - return `bundle for ${this.each[0].id}`; - default: - const ids = this.getIds(); - const last = ids.pop(); - const commas = ids.join(', '); - return `bundles for ${commas} and ${last}`; - } - } - - async ensureDir() { - await mkdirp(this.env.workingDir); - } - - async writeEntryFiles() { - await this.ensureDir(); - - for (const bundle of this.each) { - const existing = await bundle.readEntryFile(); - const expected = bundle.renderContent(); - - if (existing !== expected) { - await bundle.writeEntryFile(); - await bundle.clearBundleFile(); - } - } - } - - hashBundleEntries() { - const hash = createHash('sha1'); - for (const bundle of this.each) { - hash.update(`bundleEntryPath:${bundle.entryPath}`); - hash.update(`bundleEntryContent:${bundle.renderContent()}`); - } - return hash.digest('hex'); - } - - async getInvalidBundles() { - const invalids = new UiBundleCollection(this.env); - - for (const bundle of this.each) { - const exists = await bundle.checkForExistingOutput(); - if (!exists) { - invalids.add(bundle); - } - } - - return invalids; - } - - toWebpackEntries() { - return transform(this.each, function (entries, bundle) { - entries[bundle.id] = bundle.entryPath; - }, {}); - } - - getIds() { - return pluck(this.each, 'id'); - } - - toJSON() { - return this.each; - } -} diff --git a/src/ui/ui_bundler_env.js b/src/ui/ui_bundler_env.js deleted file mode 100644 index b0bdc3b890e430..00000000000000 --- a/src/ui/ui_bundler_env.js +++ /dev/null @@ -1,99 +0,0 @@ -import { fromRoot } from '../utils'; -import { includes, escapeRegExp } from 'lodash'; -import { isAbsolute } from 'path'; - -const arr = v => [].concat(v || []); - -export default class UiBundlerEnv { - constructor(workingDir) { - - // the location that bundle entry files and all compiles files will - // be written - this.workingDir = workingDir; - - // the context that the bundler is running in, this is not officially - // used for anything but it is serialized into the entry file to ensure - // that they are invalidated when the context changes - this.context = {}; - - // the plugins that are used to build this environment - // are tracked and embedded into the entry file so that when the - // environment changes we can rebuild the bundles - this.pluginInfo = []; - - // regular expressions which will prevent webpack from parsing the file - this.noParse = [ - /node_modules[\/\\](angular|elasticsearch-browser)[\/\\]/, - /node_modules[\/\\](mocha|moment)[\/\\]/ - ]; - - // webpack aliases, like require paths, mapping a prefix to a directory - this.aliases = { - ui: fromRoot('src/ui/public'), - ui_framework: fromRoot('ui_framework'), - packages: fromRoot('packages'), - test_harness: fromRoot('src/test_harness/public'), - querystring: 'querystring-browser', - moment$: fromRoot('webpackShims/moment'), - 'moment-timezone$': fromRoot('webpackShims/moment-timezone') - }; - - // map of which plugins created which aliases - this.aliasOwners = {}; - - // loaders that are applied to webpack modules after all other processing - // NOTE: this is intentionally not exposed as a uiExport because it leaks - // too much of the webpack implementation to plugins, but is used by test_bundle - // core plugin to inject the instrumentation loader - this.postLoaders = []; - } - - consumePlugin(plugin) { - const tag = `${plugin.id}@${plugin.version}`; - if (includes(this.pluginInfo, tag)) return; - - if (plugin.publicDir) { - this.aliases[`plugins/${plugin.id}`] = plugin.publicDir; - } - - this.pluginInfo.push(tag); - } - - exportConsumer(type) { - switch (type) { - case 'noParse': - return (plugin, spec) => { - for (const rule of arr(spec)) { - this.noParse.push(this.getNoParseRegexp(rule)); - } - }; - - case '__globalImportAliases__': - return (plugin, spec) => { - for (const key of Object.keys(spec)) { - this.aliases[key] = spec[key]; - } - }; - } - } - - addContext(key, val) { - this.context[key] = val; - } - - addPostLoader(loader) { - this.postLoaders.push(loader); - } - - getNoParseRegexp(rule) { - if (typeof rule === 'string') { - return new RegExp(`${isAbsolute(rule) ? '^' : ''}${escapeRegExp(rule)}`); - } - - if (rule instanceof RegExp) { - return rule; - } - - throw new Error('Expected noParse rule to be a string or regexp'); - } -} diff --git a/src/ui/ui_bundles/app_entry_template.js b/src/ui/ui_bundles/app_entry_template.js new file mode 100644 index 00000000000000..2f2715e9634d0b --- /dev/null +++ b/src/ui/ui_bundles/app_entry_template.js @@ -0,0 +1,14 @@ +export const appEntryTemplate = (bundle) => ` +/** + * Test entry file + * + * This is programatically created and updated, do not modify + * + * context: ${bundle.getContext()} + */ + +require('ui/chrome'); +${bundle.getRequires().join('\n')} +require('ui/chrome').bootstrap(/* xoxo */); + +`; diff --git a/src/ui/ui_bundles/index.js b/src/ui/ui_bundles/index.js new file mode 100644 index 00000000000000..b9207b5204dbfc --- /dev/null +++ b/src/ui/ui_bundles/index.js @@ -0,0 +1 @@ +export { uiBundlesMixin } from './ui_bundles_mixin'; diff --git a/src/ui/ui_bundles/ui_bundle.js b/src/ui/ui_bundles/ui_bundle.js new file mode 100644 index 00000000000000..a73b4137562b4d --- /dev/null +++ b/src/ui/ui_bundles/ui_bundle.js @@ -0,0 +1,86 @@ +import { promisify } from 'bluebird'; + +const read = promisify(require('fs').readFile); +const write = promisify(require('fs').writeFile); +const unlink = promisify(require('fs').unlink); +const stat = promisify(require('fs').stat); + +export class UiBundle { + constructor(options) { + const { + id, + modules, + template, + controller, + } = options; + + this._id = id; + this._modules = modules; + this._template = template; + this._controller = controller; + } + + getId() { + return this._id; + } + + getContext() { + return this._controller.getContext(); + } + + getEntryPath() { + return this._controller.resolvePath(`${this.getId()}.entry.js`); + } + + getOutputPath() { + return this._controller.resolvePath(`${this.getId()}.bundle.js`); + } + + getRequires() { + return this._modules.map(module => ( + `require('${module}');` + )); + } + + renderContent() { + return this._template(this); + } + + async readEntryFile() { + try { + const content = await read(this.getEntryPath()); + return content.toString('utf8'); + } + catch (e) { + return null; + } + } + + async writeEntryFile() { + return await write(this.getEntryPath(), this.renderContent(), { encoding: 'utf8' }); + } + + async clearBundleFile() { + try { await unlink(this.getOutputPath()); } + catch (e) { return null; } + } + + async isCacheValid() { + try { + await stat(this.getOutputPath()); + return true; + } + catch (e) { + return false; + } + } + + toJSON() { + return { + id: this._id, + modules: this._modules, + entryPath: this.getEntryPath(), + outputPath: this.getOutputPath() + }; + } +} diff --git a/src/ui/ui_bundles/ui_bundles_controller.js b/src/ui/ui_bundles/ui_bundles_controller.js new file mode 100644 index 00000000000000..6d68c172b8f9bc --- /dev/null +++ b/src/ui/ui_bundles/ui_bundles_controller.js @@ -0,0 +1,183 @@ +import { createHash } from 'crypto'; +import { resolve } from 'path'; + +import { UiBundle } from './ui_bundle'; +import { fromNode as fcb } from 'bluebird'; +import { makeRe } from 'minimatch'; +import mkdirp from 'mkdirp'; +import { appEntryTemplate } from './app_entry_template'; + +function getWebpackAliases(pluginSpecs) { + return pluginSpecs.reduce((aliases, spec) => { + const publicDir = spec.getPublicDir(); + + if (!publicDir) { + return aliases; + } + + return { + ...aliases, + [`plugins/${spec.getId()}`]: publicDir + }; + }, {}); +} + +export class UiBundlesController { + constructor(kbnServer) { + const { config, uiApps, uiExports, pluginSpecs } = kbnServer; + + this._workingDir = config.get('optimize.bundleDir'); + this._env = config.get('env.name'); + this._context = { + env: config.get('env.name'), + sourceMaps: config.get('optimize.sourceMaps'), + kbnVersion: config.get('pkg.version'), + buildNum: config.get('pkg.buildNum'), + plugins: pluginSpecs + .map(spec => spec.getId()) + .sort((a, b) => a.localeCompare(b)) + }; + + this._filter = makeRe(config.get('optimize.bundleFilter') || '*', { + noglobstar: true, + noext: true, + matchBase: true + }); + + this._webpackAliases = { + ...getWebpackAliases(pluginSpecs), + ...uiExports.webpackAliases + }; + this._webpackNoParseRules = uiExports.webpackNoParseRules; + this._postLoaders = []; + this._bundles = []; + + for (const uiApp of uiApps) { + this.add({ + id: uiApp.getId(), + modules: uiApp.getModules(), + template: appEntryTemplate, + }); + } + } + + add(bundleSpec) { + const { + id, + modules, + template, + } = bundleSpec; + + if (this._filter.test(id)) { + this._bundles.push(new UiBundle({ + id, + modules, + template, + controller: this, + })); + } + } + + getWebpackNoParseRules() { + return this._webpackNoParseRules; + } + + getWorkingDir() { + return this._workingDir; + } + + addPostLoader(loaderSpec) { + this._postLoaders.push(loaderSpec); + } + + getPostLoaders() { + return this._postLoaders; + } + + getAliases() { + return this._webpackAliases; + } + + isDevMode() { + return this._env === 'development'; + } + + getContext() { + return JSON.stringify(this._context, null, ' '); + } + + resolvePath(...args) { + return resolve(this._workingDir, ...args); + } + + getCachePath() { + return this.resolvePath('../.cache', this.hashBundleEntries()); + } + + getDescription() { + switch (this._bundles.length) { + case 0: + return '0 bundles'; + case 1: + return `bundle for ${this._bundles[0].id}`; + default: + const ids = this.getIds(); + const last = ids.pop(); + const commas = ids.join(', '); + return `bundles for ${commas} and ${last}`; + } + } + + async ensureDir() { + await fcb(cb => mkdirp(this._workingDir, cb)); + } + + async writeEntryFiles() { + await this.ensureDir(); + + for (const bundle of this._bundles) { + const existing = await bundle.readEntryFile(); + const expected = bundle.renderContent(); + + if (existing !== expected) { + await bundle.writeEntryFile(); + await bundle.clearBundleFile(); + } + } + } + + hashBundleEntries() { + const hash = createHash('sha1'); + for (const bundle of this._bundles) { + hash.update(`bundleEntryPath:${bundle.getEntryPath()}`); + hash.update(`bundleEntryContent:${bundle.renderContent()}`); + } + return hash.digest('hex'); + } + + async areAllBundleCachesValid() { + for (const bundle of this._bundles) { + if (!await bundle.isCacheValid()) { + return false; + } + } + + return true; + } + + toWebpackEntries() { + return this._bundles.reduce((entries, bundle) => ({ + ...entries, + [bundle.getId()]: bundle.getEntryPath(), + }), {}); + } + + getIds() { + return this._bundles + .map(bundle => bundle.getId()); + } + + toJSON() { + return this._bundles; + } +} diff --git a/src/ui/ui_bundles/ui_bundles_mixin.js b/src/ui/ui_bundles/ui_bundles_mixin.js new file mode 100644 index 00000000000000..07c739e04e83f5 --- /dev/null +++ b/src/ui/ui_bundles/ui_bundles_mixin.js @@ -0,0 +1,12 @@ +import { UiBundlesController } from './ui_bundles_controller'; + +export async function uiBundlesMixin(kbnServer) { + kbnServer.uiBundles = new UiBundlesController(kbnServer); + + const { unknown = [] } = kbnServer.uiExports; + for (const { type, spec } of unknown) { + if (type === '__bundleProvider__') { + await spec(kbnServer); + } + } +} diff --git a/src/ui/ui_exports.js b/src/ui/ui_exports.js deleted file mode 100644 index fefca9397db5c5..00000000000000 --- a/src/ui/ui_exports.js +++ /dev/null @@ -1,197 +0,0 @@ -import _ from 'lodash'; -import minimatch from 'minimatch'; - -import UiAppCollection from './ui_app_collection'; -import UiNavLinkCollection from './ui_nav_link_collection'; - -export default class UiExports { - constructor({ urlBasePath, kibanaIndexMappings }) { - this.navLinks = new UiNavLinkCollection(this); - this.apps = new UiAppCollection(this); - this.aliases = { - fieldFormatEditors: ['ui/field_format_editor/register'], - visRequestHandlers: [ - 'ui/vis/request_handlers/courier', - 'ui/vis/request_handlers/none' - ], - visResponseHandlers: [ - 'ui/vis/response_handlers/basic', - 'ui/vis/response_handlers/none', - 'ui/vis/response_handlers/tabify' - ], - visEditorTypes: [ - 'ui/vis/editors/default/default', - ], - embeddableHandlers: [ - 'plugins/kibana/visualize/embeddable/visualize_embeddable_handler_provider', - 'plugins/kibana/discover/embeddable/search_embeddable_handler_provider', - ], - }; - this.urlBasePath = urlBasePath; - this.exportConsumer = _.memoize(this.exportConsumer); - this.consumers = []; - this.bundleProviders = []; - this.defaultInjectedVars = {}; - this.injectedVarsReplacers = []; - this.kibanaIndexMappings = kibanaIndexMappings; - } - - consumePlugin(plugin) { - plugin.apps = new UiAppCollection(this); - - const types = _.keys(plugin.uiExportsSpecs); - if (!types) return false; - - const unkown = _.reject(types, this.exportConsumer, this); - if (unkown.length) { - throw new Error('unknown export types ' + unkown.join(', ') + ' in plugin ' + plugin.id); - } - - for (const consumer of this.consumers) { - consumer.consumePlugin && consumer.consumePlugin(plugin); - } - - types.forEach((type) => { - this.exportConsumer(type)(plugin, plugin.uiExportsSpecs[type]); - }); - } - - addConsumer(consumer) { - this.consumers.push(consumer); - } - - addConsumerForType(typeToConsume, consumer) { - this.consumers.push({ - exportConsumer(uiExportType) { - if (uiExportType === typeToConsume) { - return consumer; - } - } - }); - } - - exportConsumer(type) { - for (const consumer of this.consumers) { - if (!consumer.exportConsumer) continue; - const fn = consumer.exportConsumer(type); - if (fn) return fn; - } - - switch (type) { - case 'app': - case 'apps': - return (plugin, specs) => { - for (const spec of [].concat(specs || [])) { - - const app = this.apps.new(_.defaults({}, spec, { - id: plugin.id, - urlBasePath: this.urlBasePath - })); - - plugin.extendInit((server, options) => { // eslint-disable-line no-loop-func - const wrapped = app.getInjectedVars; - app.getInjectedVars = () => wrapped.call(plugin, server, options); - }); - - plugin.apps.add(app); - } - }; - - case 'link': - case 'links': - return (plugin, specs) => { - for (const spec of [].concat(specs || [])) { - this.navLinks.new(spec); - } - }; - - case 'visTypes': - case 'visResponseHandlers': - case 'visRequestHandlers': - case 'visEditorTypes': - case 'savedObjectTypes': - case 'embeddableHandlers': - case 'fieldFormats': - case 'fieldFormatEditors': - case 'spyModes': - case 'chromeNavControls': - case 'navbarExtensions': - case 'managementSections': - case 'devTools': - case 'docViews': - case 'home': - case 'hacks': - return (plugin, spec) => { - this.aliases[type] = _.union(this.aliases[type] || [], spec); - }; - - case 'visTypeEnhancers': - return (plugin, spec) => { - //used for plugins that augment capabilities of an existing visualization - this.aliases.visTypes = _.union(this.aliases.visTypes || [], spec); - }; - - case 'bundle': - return (plugin, spec) => { - this.bundleProviders.push(spec); - }; - - case 'aliases': - return (plugin, specs) => { - _.forOwn(specs, (spec, adhocType) => { - this.aliases[adhocType] = _.union(this.aliases[adhocType] || [], spec); - }); - }; - - case 'injectDefaultVars': - return (plugin, injector) => { - plugin.extendInit(async (server, options) => { - _.defaults(this.defaultInjectedVars, await injector.call(plugin, server, options)); - }); - }; - - case 'mappings': - return (plugin, mappings) => { - this.kibanaIndexMappings.addRootProperties(mappings, { plugin: plugin.id }); - }; - - case 'replaceInjectedVars': - return (plugin, replacer) => { - this.injectedVarsReplacers.push(replacer); - }; - } - } - - find(patterns) { - const aliases = this.aliases; - const names = _.keys(aliases); - const matcher = _.partialRight(minimatch.filter, { matchBase: true }); - - return _.chain(patterns) - .map(function (pattern) { - return names.filter(matcher(pattern)); - }) - .flattenDeep() - .reduce(function (found, name) { - return found.concat(aliases[name]); - }, []) - .value(); - } - - getAllApps() { - const { apps } = this; - return [...apps].concat(...apps.hidden); - } - - getApp(id) { - return this.apps.byId[id]; - } - - getHiddenApp(id) { - return this.apps.hidden.byId[id]; - } - - getBundleProviders() { - return this.bundleProviders; - } -} diff --git a/src/plugin_discovery/__tests__/collect_ui_exports.js b/src/ui/ui_exports/__tests__/collect_ui_exports.js similarity index 95% rename from src/plugin_discovery/__tests__/collect_ui_exports.js rename to src/ui/ui_exports/__tests__/collect_ui_exports.js index ab5781078baca3..9609a4a95c12b5 100644 --- a/src/plugin_discovery/__tests__/collect_ui_exports.js +++ b/src/ui/ui_exports/__tests__/collect_ui_exports.js @@ -1,7 +1,8 @@ import expect from 'expect.js'; +import { PluginPack } from '../../../plugin_discovery'; + import { collectUiExports } from '../collect_ui_exports'; -import { PluginPack } from '../plugin_pack'; const specs = new PluginPack({ path: '/dev/null', diff --git a/src/plugin_discovery/collect_ui_exports.js b/src/ui/ui_exports/collect_ui_exports.js similarity index 81% rename from src/plugin_discovery/collect_ui_exports.js rename to src/ui/ui_exports/collect_ui_exports.js index 4e9e981631c273..2bc3efe400b677 100644 --- a/src/plugin_discovery/collect_ui_exports.js +++ b/src/ui/ui_exports/collect_ui_exports.js @@ -1,6 +1,6 @@ import { UI_EXPORT_DEFAULTS } from './ui_export_defaults'; import * as uiExportTypeReducers from './ui_export_types'; -import { reduceExportSpecs } from './plugin_exports'; +import { reduceExportSpecs } from '../../plugin_discovery'; export function collectUiExports(pluginSpecs) { return reduceExportSpecs( diff --git a/src/ui/ui_exports/index.js b/src/ui/ui_exports/index.js new file mode 100644 index 00000000000000..1326e6767489d2 --- /dev/null +++ b/src/ui/ui_exports/index.js @@ -0,0 +1 @@ +export { collectUiExports } from './collect_ui_exports'; diff --git a/src/plugin_discovery/ui_export_defaults.js b/src/ui/ui_exports/ui_export_defaults.js similarity index 94% rename from src/plugin_discovery/ui_export_defaults.js rename to src/ui/ui_exports/ui_export_defaults.js index 7cf5d855ec3cbe..d911c85ff4380d 100644 --- a/src/plugin_discovery/ui_export_defaults.js +++ b/src/ui/ui_exports/ui_export_defaults.js @@ -1,5 +1,5 @@ import { dirname, resolve } from 'path'; -const ROOT = dirname(require.resolve('../../package.json')); +const ROOT = dirname(require.resolve('../../../package.json')); export const UI_EXPORT_DEFAULTS = { webpackNoParseRules: [ diff --git a/src/plugin_discovery/ui_export_types/index.js b/src/ui/ui_exports/ui_export_types/index.js similarity index 100% rename from src/plugin_discovery/ui_export_types/index.js rename to src/ui/ui_exports/ui_export_types/index.js diff --git a/src/plugin_discovery/ui_export_types/modify_injected_vars.js b/src/ui/ui_exports/ui_export_types/modify_injected_vars.js similarity index 100% rename from src/plugin_discovery/ui_export_types/modify_injected_vars.js rename to src/ui/ui_exports/ui_export_types/modify_injected_vars.js diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/alias.js b/src/ui/ui_exports/ui_export_types/modify_reduce/alias.js similarity index 100% rename from src/plugin_discovery/ui_export_types/modify_reduce/alias.js rename to src/ui/ui_exports/ui_export_types/modify_reduce/alias.js diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/debug.js b/src/ui/ui_exports/ui_export_types/modify_reduce/debug.js similarity index 100% rename from src/plugin_discovery/ui_export_types/modify_reduce/debug.js rename to src/ui/ui_exports/ui_export_types/modify_reduce/debug.js diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/index.js b/src/ui/ui_exports/ui_export_types/modify_reduce/index.js similarity index 100% rename from src/plugin_discovery/ui_export_types/modify_reduce/index.js rename to src/ui/ui_exports/ui_export_types/modify_reduce/index.js diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/map_spec.js b/src/ui/ui_exports/ui_export_types/modify_reduce/map_spec.js similarity index 100% rename from src/plugin_discovery/ui_export_types/modify_reduce/map_spec.js rename to src/ui/ui_exports/ui_export_types/modify_reduce/map_spec.js diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/unique_keys.js b/src/ui/ui_exports/ui_export_types/modify_reduce/unique_keys.js similarity index 100% rename from src/plugin_discovery/ui_export_types/modify_reduce/unique_keys.js rename to src/ui/ui_exports/ui_export_types/modify_reduce/unique_keys.js diff --git a/src/plugin_discovery/ui_export_types/modify_reduce/wrap.js b/src/ui/ui_exports/ui_export_types/modify_reduce/wrap.js similarity index 100% rename from src/plugin_discovery/ui_export_types/modify_reduce/wrap.js rename to src/ui/ui_exports/ui_export_types/modify_reduce/wrap.js diff --git a/src/plugin_discovery/ui_export_types/reduce/concat.js b/src/ui/ui_exports/ui_export_types/reduce/concat.js similarity index 100% rename from src/plugin_discovery/ui_export_types/reduce/concat.js rename to src/ui/ui_exports/ui_export_types/reduce/concat.js diff --git a/src/plugin_discovery/ui_export_types/reduce/concat_values.js b/src/ui/ui_exports/ui_export_types/reduce/concat_values.js similarity index 100% rename from src/plugin_discovery/ui_export_types/reduce/concat_values.js rename to src/ui/ui_exports/ui_export_types/reduce/concat_values.js diff --git a/src/plugin_discovery/ui_export_types/reduce/index.js b/src/ui/ui_exports/ui_export_types/reduce/index.js similarity index 100% rename from src/plugin_discovery/ui_export_types/reduce/index.js rename to src/ui/ui_exports/ui_export_types/reduce/index.js diff --git a/src/plugin_discovery/ui_export_types/reduce/merge.js b/src/ui/ui_exports/ui_export_types/reduce/merge.js similarity index 100% rename from src/plugin_discovery/ui_export_types/reduce/merge.js rename to src/ui/ui_exports/ui_export_types/reduce/merge.js diff --git a/src/plugin_discovery/ui_export_types/reduce/merge_type.js b/src/ui/ui_exports/ui_export_types/reduce/merge_type.js similarity index 100% rename from src/plugin_discovery/ui_export_types/reduce/merge_type.js rename to src/ui/ui_exports/ui_export_types/reduce/merge_type.js diff --git a/src/plugin_discovery/ui_export_types/reduce/unique_assign.js b/src/ui/ui_exports/ui_export_types/reduce/unique_assign.js similarity index 100% rename from src/plugin_discovery/ui_export_types/reduce/unique_assign.js rename to src/ui/ui_exports/ui_export_types/reduce/unique_assign.js diff --git a/src/plugin_discovery/ui_export_types/saved_object_mappings.js b/src/ui/ui_exports/ui_export_types/saved_object_mappings.js similarity index 100% rename from src/plugin_discovery/ui_export_types/saved_object_mappings.js rename to src/ui/ui_exports/ui_export_types/saved_object_mappings.js diff --git a/src/plugin_discovery/ui_export_types/ui_apps.js b/src/ui/ui_exports/ui_export_types/ui_apps.js similarity index 100% rename from src/plugin_discovery/ui_export_types/ui_apps.js rename to src/ui/ui_exports/ui_export_types/ui_apps.js diff --git a/src/plugin_discovery/ui_export_types/ui_extensions.js b/src/ui/ui_exports/ui_export_types/ui_extensions.js similarity index 97% rename from src/plugin_discovery/ui_export_types/ui_extensions.js rename to src/ui/ui_exports/ui_export_types/ui_extensions.js index 52f7492dee6cf6..daba06506b575a 100644 --- a/src/plugin_discovery/ui_export_types/ui_extensions.js +++ b/src/ui/ui_exports/ui_export_types/ui_extensions.js @@ -29,6 +29,7 @@ export const managementSections = appExtension; export const devTools = appExtension; export const docViews = appExtension; export const hacks = appExtension; +export const home = appExtension; // aliases visTypeEnhancers to the visTypes group export const visTypeEnhancers = wrap(alias('visTypes'), appExtension); diff --git a/src/plugin_discovery/ui_export_types/ui_i18n.js b/src/ui/ui_exports/ui_export_types/ui_i18n.js similarity index 100% rename from src/plugin_discovery/ui_export_types/ui_i18n.js rename to src/ui/ui_exports/ui_export_types/ui_i18n.js diff --git a/src/plugin_discovery/ui_export_types/ui_nav_links.js b/src/ui/ui_exports/ui_export_types/ui_nav_links.js similarity index 100% rename from src/plugin_discovery/ui_export_types/ui_nav_links.js rename to src/ui/ui_exports/ui_export_types/ui_nav_links.js diff --git a/src/plugin_discovery/ui_export_types/ui_settings.js b/src/ui/ui_exports/ui_export_types/ui_settings.js similarity index 100% rename from src/plugin_discovery/ui_export_types/ui_settings.js rename to src/ui/ui_exports/ui_export_types/ui_settings.js diff --git a/src/plugin_discovery/ui_export_types/unknown.js b/src/ui/ui_exports/ui_export_types/unknown.js similarity index 100% rename from src/plugin_discovery/ui_export_types/unknown.js rename to src/ui/ui_exports/ui_export_types/unknown.js diff --git a/src/ui/ui_exports/ui_export_types/webpack_customizations.js b/src/ui/ui_exports/ui_export_types/webpack_customizations.js new file mode 100644 index 00000000000000..899aa1ee9f1fa6 --- /dev/null +++ b/src/ui/ui_exports/ui_export_types/webpack_customizations.js @@ -0,0 +1,23 @@ +import { isAbsolute } from 'path'; + +import { escapeRegExp } from 'lodash'; + +import { concat, merge } from './reduce'; +import { alias, wrap, uniqueKeys, mapSpec } from './modify_reduce'; + +export const __globalImportAliases__ = wrap(alias('webpackAliases'), uniqueKeys('__globalImportAliases__'), merge); +export const noParse = wrap( + alias('webpackNoParseRules'), + mapSpec(rule => { + if (typeof rule === 'string') { + return new RegExp(`${isAbsolute(rule) ? '^' : ''}${escapeRegExp(rule)}`); + } + + if (rule instanceof RegExp) { + return rule; + } + + throw new Error('Expected noParse rule to be a string or regexp'); + }), + concat +); diff --git a/src/ui/ui_i18n.js b/src/ui/ui_i18n.js deleted file mode 100644 index 0895ec1add423c..00000000000000 --- a/src/ui/ui_i18n.js +++ /dev/null @@ -1,66 +0,0 @@ -import { resolve } from 'path'; - -import { defaults, compact } from 'lodash'; -import langParser from 'accept-language-parser'; - -import { I18n } from './i18n'; - -function acceptLanguageHeaderToBCP47Tags(header) { - return langParser.parse(header).map(lang => ( - compact([lang.code, lang.region, lang.script]).join('-') - )); -} - -export class UiI18n { - constructor(defaultLocale = 'en') { - this._i18n = new I18n(defaultLocale); - this._i18n.registerTranslations(resolve(__dirname, './translations/en.json')); - } - - /** - * Fetch the language translations as defined by the request. - * - * @param {Hapi.Request} request - * @returns {Promise} translations promise for an object where - * keys are translation keys and - * values are translations - */ - async getTranslationsForRequest(request) { - const header = request.headers['accept-language']; - const tags = acceptLanguageHeaderToBCP47Tags(header); - const requestedTranslations = await this._i18n.getTranslations(...tags); - const defaultTranslations = await this._i18n.getTranslationsForDefaultLocale(); - return defaults({}, requestedTranslations, defaultTranslations); - } - - /** - * uiExport consumers help the uiExports module know what to - * do with the uiExports defined by each plugin. - * - * This consumer will allow plugins to define export with the - * "translations" type like so: - * - * new kibana.Plugin({ - * uiExports: { - * translations: [ - * resolve(__dirname, './translations/es.json'), - * ], - * }, - * }); - * - */ - addUiExportConsumer(uiExports) { - uiExports.addConsumerForType('translations', (plugin, translations) => { - translations.forEach(path => { - this._i18n.registerTranslations(path); - }); - }); - } - - /** - Refer to I18n.getAllTranslations() - */ - getAllTranslations() { - return this._i18n.getAllTranslations(); - } -} diff --git a/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/de.json b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/de.json new file mode 100644 index 00000000000000..ab9171f3a86cf1 --- /dev/null +++ b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/de.json @@ -0,0 +1,4 @@ +{ + "test_plugin_1-NO_SSL": "Dont run the DE dev server using HTTPS", + "test_plugin_1-DEV": "Run the DE server with development mode defaults" +} diff --git a/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/en.json b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/en.json new file mode 100644 index 00000000000000..53dddcb859f709 --- /dev/null +++ b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/en.json @@ -0,0 +1,6 @@ +{ + "test_plugin_1-NO_SSL": "Dont run the dev server using HTTPS", + "test_plugin_1-DEV": "Run the server with development mode defaults", + "test_plugin_1-NO_RUN_SERVER": "Dont run the dev server", + "test_plugin_1-HOME": "Run along home now!" +} diff --git a/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/es-ES.json b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/es-ES.json new file mode 100644 index 00000000000000..4a7ce753d9354f --- /dev/null +++ b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_1/es-ES.json @@ -0,0 +1,3 @@ +{ + "test_plugin_1-NO_SSL": "Dont run the es-ES dev server using HTTPS! I am regsitered afterwards!" +} diff --git a/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_2/de.json b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_2/de.json new file mode 100644 index 00000000000000..d87b0a4f3a88ce --- /dev/null +++ b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_2/de.json @@ -0,0 +1,3 @@ +{ + "test_plugin_1-NO_SSL": "Dont run the DE dev server using HTTPS! I am regsitered afterwards!" +} diff --git a/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_2/en.json b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_2/en.json new file mode 100644 index 00000000000000..3e791c7d6e7764 --- /dev/null +++ b/src/ui/ui_i18n/__tests__/fixtures/translations/test_plugin_2/en.json @@ -0,0 +1,6 @@ +{ + "test_plugin_2-XXXXXX": "This is XXXXXX string", + "test_plugin_2-YYYY_PPPP": "This is YYYY_PPPP string", + "test_plugin_2-FFFFFFFFFFFF": "This is FFFFFFFFFFFF string", + "test_plugin_2-ZZZ": "This is ZZZ string" +} diff --git a/src/ui/ui_i18n/__tests__/i18n.js b/src/ui/ui_i18n/__tests__/i18n.js new file mode 100644 index 00000000000000..f903f0dbd9a4d9 --- /dev/null +++ b/src/ui/ui_i18n/__tests__/i18n.js @@ -0,0 +1,226 @@ +import expect from 'expect.js'; +import _ from 'lodash'; +import { join } from 'path'; + +import { I18n } from '../'; + +const FIXTURES = join(__dirname, 'fixtures'); + +describe('ui/i18n module', function () { + + describe('one plugin', function () { + + const i18nObj = new I18n(); + + before('registerTranslations - one plugin', function () { + const pluginName = 'test_plugin_1'; + const pluginTranslationPath = join(FIXTURES, 'translations', pluginName); + const translationFiles = [ + join(pluginTranslationPath, 'de.json'), + join(pluginTranslationPath, 'en.json') + ]; + const filesLen = translationFiles.length; + for (let indx = 0; indx < filesLen; indx++) { + i18nObj.registerTranslations(translationFiles[indx]); + } + }); + + describe('getTranslations', function () { + + it('should return the translations for en locale as registered', function () { + const languageTag = ['en']; + const expectedTranslationJson = { + 'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS', + 'test_plugin_1-DEV': 'Run the server with development mode defaults', + 'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server', + 'test_plugin_1-HOME': 'Run along home now!' + }; + return checkTranslations(expectedTranslationJson, languageTag, i18nObj); + }); + + it('should return the translations for de locale as registered', function () { + const languageTag = ['de']; + const expectedTranslationJson = { + 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', + 'test_plugin_1-DEV': 'Run the DE server with development mode defaults' + }; + return checkTranslations(expectedTranslationJson, languageTag, i18nObj); + }); + + it('should pick the highest priority language for which translations exist', function () { + const languageTags = ['es-ES', 'de', 'en']; + const expectedTranslations = { + 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', + 'test_plugin_1-DEV': 'Run the DE server with development mode defaults', + }; + return checkTranslations(expectedTranslations, languageTags, i18nObj); + }); + + it('should return translations for highest priority locale where best case match is chosen from registered locales', function () { + const languageTags = ['es', 'de']; + const expectedTranslations = { + 'test_plugin_1-NO_SSL': 'Dont run the es-ES dev server using HTTPS! I am regsitered afterwards!' + }; + i18nObj.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_1', 'es-ES.json')); + return checkTranslations(expectedTranslations, languageTags, i18nObj); + }); + + it('should return an empty object for locales with no translations', function () { + const languageTags = ['ja-JA', 'fr']; + return checkTranslations({}, languageTags, i18nObj); + }); + + }); + + describe('getTranslationsForDefaultLocale', function () { + + it('should return translations for default locale which is set to the en locale', function () { + const i18nObj1 = new I18n('en'); + const expectedTranslations = { + 'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS', + 'test_plugin_1-DEV': 'Run the server with development mode defaults', + 'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server', + 'test_plugin_1-HOME': 'Run along home now!' + }; + i18nObj1.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_1', 'en.json')); + return checkTranslationsForDefaultLocale(expectedTranslations, i18nObj1); + }); + + it('should return translations for default locale which is set to the de locale', function () { + const i18nObj1 = new I18n('de'); + const expectedTranslations = { + 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', + 'test_plugin_1-DEV': 'Run the DE server with development mode defaults', + }; + i18nObj1.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_1', 'de.json')); + return checkTranslationsForDefaultLocale(expectedTranslations, i18nObj1); + }); + + }); + + describe('getAllTranslations', function () { + + it('should return all translations', function () { + const expectedTranslations = { + de: { + 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', + 'test_plugin_1-DEV': 'Run the DE server with development mode defaults' + }, + en: { + 'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS', + 'test_plugin_1-DEV': 'Run the server with development mode defaults', + 'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server', + 'test_plugin_1-HOME': 'Run along home now!' + }, + 'es-ES': { + 'test_plugin_1-NO_SSL': 'Dont run the es-ES dev server using HTTPS! I am regsitered afterwards!' + } + }; + return checkAllTranslations(expectedTranslations, i18nObj); + }); + + }); + + }); + + describe('multiple plugins', function () { + + const i18nObj = new I18n(); + + beforeEach('registerTranslations - multiple plugin', function () { + const pluginTranslationPath = join(FIXTURES, 'translations'); + const translationFiles = [ + join(pluginTranslationPath, 'test_plugin_1', 'de.json'), + join(pluginTranslationPath, 'test_plugin_1', 'en.json'), + join(pluginTranslationPath, 'test_plugin_2', 'en.json') + ]; + const filesLen = translationFiles.length; + for (let indx = 0; indx < filesLen; indx++) { + i18nObj.registerTranslations(translationFiles[indx]); + } + }); + + describe('getTranslations', function () { + + it('should return the translations for en locale as registered', function () { + const languageTag = ['en']; + const expectedTranslationJson = { + 'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS', + 'test_plugin_1-DEV': 'Run the server with development mode defaults', + 'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server', + 'test_plugin_1-HOME': 'Run along home now!', + 'test_plugin_2-XXXXXX': 'This is XXXXXX string', + 'test_plugin_2-YYYY_PPPP': 'This is YYYY_PPPP string', + 'test_plugin_2-FFFFFFFFFFFF': 'This is FFFFFFFFFFFF string', + 'test_plugin_2-ZZZ': 'This is ZZZ string' + }; + return checkTranslations(expectedTranslationJson, languageTag, i18nObj); + }); + + it('should return the translations for de locale as registered', function () { + const languageTag = ['de']; + const expectedTranslationJson = { + 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', + 'test_plugin_1-DEV': 'Run the DE server with development mode defaults' + }; + return checkTranslations(expectedTranslationJson, languageTag, i18nObj); + }); + + it('should return the most recently registered translation for a key that has multiple translations', function () { + i18nObj.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_2', 'de.json')); + const languageTag = ['de']; + const expectedTranslationJson = { + 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS! I am regsitered afterwards!', + 'test_plugin_1-DEV': 'Run the DE server with development mode defaults' + }; + return checkTranslations(expectedTranslationJson, languageTag, i18nObj); + }); + + }); + + }); + + describe('registerTranslations', function () { + + const i18nObj = new I18n(); + + it('should throw error when registering relative path', function () { + return expect(i18nObj.registerTranslations).withArgs('./some/path').to.throwError(); + }); + + it('should throw error when registering empty filename', function () { + return expect(i18nObj.registerTranslations).withArgs('').to.throwError(); + }); + + it('should throw error when registering filename with no extension', function () { + return expect(i18nObj.registerTranslations).withArgs('file1').to.throwError(); + }); + + it('should throw error when registering filename with non JSON extension', function () { + return expect(i18nObj.registerTranslations).withArgs('file1.txt').to.throwError(); + }); + + }); + +}); + +function checkTranslations(expectedTranslations, languageTags, i18nObj) { + return i18nObj.getTranslations(...languageTags) + .then(function (actualTranslations) { + expect(_.isEqual(actualTranslations, expectedTranslations)).to.be(true); + }); +} + +function checkAllTranslations(expectedTranslations, i18nObj) { + return i18nObj.getAllTranslations() + .then(function (actualTranslations) { + expect(_.isEqual(actualTranslations, expectedTranslations)).to.be(true); + }); +} + +function checkTranslationsForDefaultLocale(expectedTranslations, i18nObj) { + return i18nObj.getTranslationsForDefaultLocale() + .then(function (actualTranslations) { + expect(_.isEqual(actualTranslations, expectedTranslations)).to.be(true); + }); +} diff --git a/src/ui/ui_i18n/i18n.js b/src/ui/ui_i18n/i18n.js new file mode 100644 index 00000000000000..8e1d4984aff201 --- /dev/null +++ b/src/ui/ui_i18n/i18n.js @@ -0,0 +1,136 @@ +import path from 'path'; +import Promise from 'bluebird'; +import { readFile } from 'fs'; +import _ from 'lodash'; + +const asyncReadFile = Promise.promisify(readFile); + +const TRANSLATION_FILE_EXTENSION = '.json'; + +function getLocaleFromFileName(fullFileName) { + if (_.isEmpty(fullFileName)) throw new Error('Filename empty'); + + const fileExt = path.extname(fullFileName); + if (fileExt.length <= 0 || fileExt !== TRANSLATION_FILE_EXTENSION) { + throw new Error('Translations must be in a JSON file. File being registered is ' + fullFileName); + } + + return path.basename(fullFileName, TRANSLATION_FILE_EXTENSION); +} + +function getBestLocaleMatch(languageTag, registeredLocales) { + if (_.contains(registeredLocales, languageTag)) { + return languageTag; + } + + // Find the first registered locale that begins with one of the language codes from the provided language tag. + // For example, if there is an 'en' language code, it would match an 'en-US' registered locale. + const languageCode = _.first(languageTag.split('-')) || []; + return _.find(registeredLocales, (locale) => _.startsWith(locale, languageCode)); +} + +export class I18n { + + _registeredTranslations = {}; + + constructor(defaultLocale = 'en') { + this._defaultLocale = defaultLocale; + } + + /** + * Return all translations for registered locales + * @return {Promise} translations - A Promise object where keys are + * the locale and values are Objects + * of translation keys and translations + */ + getAllTranslations() { + const localeTranslations = {}; + + const locales = this._getRegisteredTranslationLocales(); + const translations = _.map(locales, (locale) => { + return this._getTranslationsForLocale(locale) + .then(function (translations) { + localeTranslations[locale] = translations; + }); + }); + + return Promise.all(translations) + .then(() => _.assign({}, localeTranslations)); + } + + /** + * Return translations for a suitable locale from a user side locale list + * @param {...string} languageTags - BCP 47 language tags. The tags are listed in priority order as set in the Accept-Language header. + * @returns {Promise} translations - promise for an object where + * keys are translation keys and + * values are translations + * This object will contain all registered translations for the highest priority locale which is registered with the i18n module. + * This object can be empty if no locale in the language tags can be matched against the registered locales. + */ + getTranslations(...languageTags) { + const locale = this._getTranslationLocale(languageTags); + return this._getTranslationsForLocale(locale); + } + + /** + * Return all translations registered for the default locale. + * @returns {Promise} translations - promise for an object where + * keys are translation keys and + * values are translations + */ + getTranslationsForDefaultLocale() { + return this._getTranslationsForLocale(this._defaultLocale); + } + + /** + * The translation file is registered with i18n plugin. The plugin contains a list of registered translation file paths per language. + * @param {String} absolutePluginTranslationFilePath - Absolute path to the translation file to register. + */ + registerTranslations(absolutePluginTranslationFilePath) { + if (!path.isAbsolute(absolutePluginTranslationFilePath)) { + throw new TypeError( + 'Paths to translation files must be absolute. ' + + `Got relative path: "${absolutePluginTranslationFilePath}"` + ); + } + + const locale = getLocaleFromFileName(absolutePluginTranslationFilePath); + + this._registeredTranslations[locale] = + _.uniq(_.get(this._registeredTranslations, locale, []).concat(absolutePluginTranslationFilePath)); + } + + _getRegisteredTranslationLocales() { + return Object.keys(this._registeredTranslations); + } + + _getTranslationLocale(languageTags) { + let locale = ''; + const registeredLocales = this._getRegisteredTranslationLocales(); + _.forEach(languageTags, (tag) => { + locale = locale || getBestLocaleMatch(tag, registeredLocales); + }); + return locale; + } + + _getTranslationsForLocale(locale) { + if (!this._registeredTranslations.hasOwnProperty(locale)) { + return Promise.resolve({}); + } + + const translationFiles = this._registeredTranslations[locale]; + const translations = _.map(translationFiles, (filename) => { + return asyncReadFile(filename, 'utf8') + .then(fileContents => JSON.parse(fileContents)) + .catch(SyntaxError, function () { + throw new Error('Invalid json in ' + filename); + }) + .catch(function () { + throw new Error('Cannot read file ' + filename); + }); + }); + + return Promise.all(translations) + .then(translations => _.assign({}, ...translations)); + } +} diff --git a/src/ui/ui_i18n/index.js b/src/ui/ui_i18n/index.js new file mode 100644 index 00000000000000..166b45aa99ae0a --- /dev/null +++ b/src/ui/ui_i18n/index.js @@ -0,0 +1,2 @@ +export { I18n } from './i18n'; +export { uiI18nMixin } from './ui_i18n_mixin'; diff --git a/src/ui/ui_i18n/translations/en.json b/src/ui/ui_i18n/translations/en.json new file mode 100644 index 00000000000000..ac491cf6f34657 --- /dev/null +++ b/src/ui/ui_i18n/translations/en.json @@ -0,0 +1,4 @@ +{ + "UI-WELCOME_MESSAGE": "Loading", + "UI-WELCOME_ERROR": "" +} diff --git a/src/ui/ui_i18n/ui_i18n_mixin.js b/src/ui/ui_i18n/ui_i18n_mixin.js new file mode 100644 index 00000000000000..c11b4c765a494a --- /dev/null +++ b/src/ui/ui_i18n/ui_i18n_mixin.js @@ -0,0 +1,49 @@ +import { defaults, compact } from 'lodash'; +import langParser from 'accept-language-parser'; + +import { I18n } from './i18n'; + +function acceptLanguageHeaderToBCP47Tags(header) { + return langParser.parse(header).map(lang => ( + compact([lang.code, lang.region, lang.script]).join('-') + )); +} + +export function uiI18nMixin(kbnServer, server, config) { + const defaultLocale = config.get('i18n.defaultLocale'); + + const i18n = new I18n(defaultLocale); + const { translationPaths = [] } = kbnServer.uiExports; + translationPaths.forEach(translationPath => { + i18n.registerTranslations(translationPath); + }); + + /** + * Fetch the translations matching the Accept-Language header for a requests. + * @name request.getUiTranslations + * @returns {Promise>} translations + */ + server.decorate('request', 'getUiTranslations', async function () { + const header = this.headers['accept-language']; + const tags = acceptLanguageHeaderToBCP47Tags(header); + + const requestedTranslations = await i18n.getTranslations(...tags); + const defaultTranslations = await i18n.getTranslationsForDefaultLocale(); + + return defaults( + {}, + requestedTranslations, + defaultTranslations + ); + }); + + /** + * Return all translations for registered locales + * @name server.getAllUiTranslations + * @return {Promise>>} + */ + server.decorate('server', 'getAllUiTranslations', async () => { + return await i18n.getAllTranslations(); + }); + +} diff --git a/src/ui/ui_mixin.js b/src/ui/ui_mixin.js new file mode 100644 index 00000000000000..753afe93f1e3f3 --- /dev/null +++ b/src/ui/ui_mixin.js @@ -0,0 +1,22 @@ +import { collectUiExports } from './ui_exports'; +import { fieldFormatsMixin } from './field_formats'; +import { uiAppsMixin } from './ui_apps'; +import { uiI18nMixin } from './ui_i18n'; +import { uiBundlesMixin } from './ui_bundles'; +import { uiNavLinksMixin } from './ui_nav_links'; +import { uiRenderMixin } from './ui_render'; +import { uiSettingsMixin } from './ui_settings'; + +export async function uiMixin(kbnServer) { + kbnServer.uiExports = collectUiExports( + kbnServer.pluginSpecs + ); + + await kbnServer.mixin(uiAppsMixin); + await kbnServer.mixin(uiBundlesMixin); + await kbnServer.mixin(uiSettingsMixin); + await kbnServer.mixin(fieldFormatsMixin); + await kbnServer.mixin(uiNavLinksMixin); + await kbnServer.mixin(uiI18nMixin); + await kbnServer.mixin(uiRenderMixin); +} diff --git a/src/ui/ui_nav_link.js b/src/ui/ui_nav_link.js deleted file mode 100644 index 409df41d2dc6cd..00000000000000 --- a/src/ui/ui_nav_link.js +++ /dev/null @@ -1,15 +0,0 @@ -export default class UiNavLink { - constructor(uiExports, spec) { - this.id = spec.id; - this.title = spec.title; - this.order = spec.order || 0; - this.url = `${uiExports.urlBasePath || ''}${spec.url}`; - this.subUrlBase = `${uiExports.urlBasePath || ''}${spec.subUrlBase || spec.url}`; - this.description = spec.description; - this.icon = spec.icon; - this.linkToLastSubUrl = spec.linkToLastSubUrl === false ? false : true; - this.hidden = spec.hidden || false; - this.disabled = spec.disabled || false; - this.tooltip = spec.tooltip || ''; - } -} diff --git a/src/ui/ui_nav_link_collection.js b/src/ui/ui_nav_link_collection.js deleted file mode 100644 index 8e0b274a439b3e..00000000000000 --- a/src/ui/ui_nav_link_collection.js +++ /dev/null @@ -1,34 +0,0 @@ -import { sortBy } from 'lodash'; -import UiNavLink from './ui_nav_link'; -import Collection from '../utils/collection'; - -const inOrderCache = Symbol('inOrder'); - -export default class UiNavLinkCollection extends Collection { - - constructor(uiExports) { - super(); - this.uiExports = uiExports; - } - - new(spec) { - const link = new UiNavLink(this.uiExports, spec); - this[inOrderCache] = null; - this.add(link); - return link; - } - - get inOrder() { - if (!this[inOrderCache]) { - this[inOrderCache] = sortBy([...this], 'order'); - } - - return this[inOrderCache]; - } - - delete(value) { - this[inOrderCache] = null; - return super.delete(value); - } - -} diff --git a/src/ui/ui_nav_links/index.js b/src/ui/ui_nav_links/index.js new file mode 100644 index 00000000000000..14968e51cda4ec --- /dev/null +++ b/src/ui/ui_nav_links/index.js @@ -0,0 +1,2 @@ +export { UiNavLink } from './ui_nav_link'; +export { uiNavLinksMixin } from './ui_nav_links_mixin'; diff --git a/src/ui/ui_nav_links/ui_nav_link.js b/src/ui/ui_nav_links/ui_nav_link.js new file mode 100644 index 00000000000000..322717c446d96f --- /dev/null +++ b/src/ui/ui_nav_links/ui_nav_link.js @@ -0,0 +1,49 @@ +export class UiNavLink { + constructor(urlBasePath, spec) { + const { + id, + title, + order = 0, + url, + subUrlBase, + description, + icon, + linkToLastSubUrl = true, + hidden = false, + disabled = false, + tooltip = '', + } = spec; + + this._id = id; + this._title = title; + this._order = order; + this._url = `${urlBasePath || ''}${url}`; + this._subUrlBase = `${urlBasePath || ''}${subUrlBase || url}`; + this._description = description; + this._icon = icon; + this._linkToLastSubUrl = linkToLastSubUrl; + this._hidden = hidden; + this._disabled = disabled; + this._tooltip = tooltip; + } + + getOrder() { + return this._order; + } + + toJSON() { + return { + id: this._id, + title: this._title, + order: this._order, + url: this._url, + subUrlBase: this._subUrlBase, + description: this._description, + icon: this._icon, + linkToLastSubUrl: this._linkToLastSubUrl, + hidden: this._hidden, + disabled: this._disabled, + tooltip: this._tooltip, + }; + } +} diff --git a/src/ui/ui_nav_links/ui_nav_links_mixin.js b/src/ui/ui_nav_links/ui_nav_links_mixin.js new file mode 100644 index 00000000000000..38411c799bf348 --- /dev/null +++ b/src/ui/ui_nav_links/ui_nav_links_mixin.js @@ -0,0 +1,23 @@ +import { UiNavLink } from './ui_nav_link'; + +export function uiNavLinksMixin(kbnServer, server, config) { + const uiApps = server.getAllUiApps(); + + const { navLinkSpecs = [] } = kbnServer.uiExports; + const urlBasePath = config.get('server.basePath'); + + const fromSpecs = navLinkSpecs + .map(navLinkSpec => new UiNavLink(urlBasePath, navLinkSpec)); + + const fromApps = uiApps + .map(app => app.getNavLink()) + .filter(Boolean); + + const uiNavLinks = fromSpecs + .concat(fromApps) + .sort((a, b) => a.getOrder() - b.getOrder()); + + server.decorate('server', 'getUiNavLinks', () => ( + uiNavLinks.slice(0) + )); +} diff --git a/src/ui/ui_render/index.js b/src/ui/ui_render/index.js new file mode 100644 index 00000000000000..79bd880c2499dc --- /dev/null +++ b/src/ui/ui_render/index.js @@ -0,0 +1 @@ +export { uiRenderMixin } from './ui_render_mixin'; diff --git a/src/ui/ui_render/ui_render_mixin.js b/src/ui/ui_render/ui_render_mixin.js new file mode 100644 index 00000000000000..4a97cc48f7cbaf --- /dev/null +++ b/src/ui/ui_render/ui_render_mixin.js @@ -0,0 +1,128 @@ +import { defaults, get } from 'lodash'; +import { props, reduce as reduceAsync } from 'bluebird'; +import Boom from 'boom'; +import { resolve } from 'path'; + +function getDefaultInjectedVars(kbnServer) { + const { defaultInjectedVarProviders = [] } = kbnServer.uiExports; + + return defaultInjectedVarProviders.reduce((allDefaults, { fn, pluginSpec }) => ( + defaults( + allDefaults, + fn(kbnServer.server, pluginSpec.readConfigValue(kbnServer.config, [])) + ) + ), {}); +} + +function getReplaceInjectedVars(kbnServer) { + const { injectedVarsReplacers = [] } = kbnServer.uiExports; + + return function replaceInjectedVars(request, injectedVars) { + return reduceAsync( + injectedVarsReplacers, + async (acc, replacer) => await replacer(acc, request, kbnServer.server), + injectedVars + ); + }; +} + +export function uiRenderMixin(kbnServer, server, config) { + const replaceInjectedVars = getReplaceInjectedVars(kbnServer); + + let defaultInjectedVars = {}; + kbnServer.afterPluginsInit(() => { + defaultInjectedVars = getDefaultInjectedVars(kbnServer); + }); + + // render all views from ./views + server.setupViews(resolve(__dirname, 'views')); + + server.route({ + path: '/app/{id}', + method: 'GET', + async handler(req, reply) { + const id = req.params.id; + const app = server.getUiAppById(id); + if (!app) return reply(Boom.notFound('Unknown app ' + id)); + + try { + if (kbnServer.status.isGreen()) { + await reply.renderApp(app); + } else { + await reply.renderStatusPage(); + } + } catch (err) { + reply(Boom.wrap(err)); + } + } + }); + + async function getKibanaPayload({ app, request, includeUserProvidedConfig, injectedVarsOverrides }) { + const uiSettings = request.getUiSettingsService(); + const translations = await request.getUiTranslations(); + + return { + app: app, + bundleId: `app:${app.getId()}`, + nav: server.getUiNavLinks(), + version: kbnServer.version, + branch: config.get('pkg.branch'), + buildNum: config.get('pkg.buildNum'), + buildSha: config.get('pkg.buildSha'), + basePath: config.get('server.basePath'), + serverName: config.get('server.name'), + devMode: config.get('env.dev'), + translations: translations, + uiSettings: await props({ + defaults: uiSettings.getDefaults(), + user: includeUserProvidedConfig && uiSettings.getUserProvided() + }), + vars: await replaceInjectedVars( + request, + defaults( + injectedVarsOverrides, + await app.getInjectedVars() || {}, + defaultInjectedVars, + ), + ) + }; + } + + async function renderApp({ app, reply, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) { + try { + const request = reply.request; + const translations = await request.getUiTranslations(); + + return reply.view(app.getTemplateName(), { + app, + kibanaPayload: await getKibanaPayload({ + app, + request, + includeUserProvidedConfig, + injectedVarsOverrides + }), + bundlePath: `${config.get('server.basePath')}/bundles`, + i18n: key => get(translations, key, ''), + }); + } catch (err) { + reply(err); + } + } + + server.decorate('reply', 'renderApp', function (app, injectedVarsOverrides) { + return renderApp({ + app, + reply: this, + includeUserProvidedConfig: true, + injectedVarsOverrides, + }); + }); + + server.decorate('reply', 'renderAppWithDefaultConfig', function (app) { + return renderApp({ + app, + reply: this, + includeUserProvidedConfig: false, + }); + }); +} diff --git a/src/ui/views/chrome.jade b/src/ui/ui_render/views/chrome.jade similarity index 100% rename from src/ui/views/chrome.jade rename to src/ui/ui_render/views/chrome.jade diff --git a/src/ui/views/root_redirect.jade b/src/ui/ui_render/views/root_redirect.jade similarity index 100% rename from src/ui/views/root_redirect.jade rename to src/ui/ui_render/views/root_redirect.jade diff --git a/src/ui/views/ui_app.jade b/src/ui/ui_render/views/ui_app.jade similarity index 98% rename from src/ui/views/ui_app.jade rename to src/ui/ui_render/views/ui_app.jade index ee89f8aae99dd8..1595059410bfa0 100644 --- a/src/ui/views/ui_app.jade +++ b/src/ui/ui_render/views/ui_app.jade @@ -121,9 +121,9 @@ block content } var files = [ bundleFile('commons.style.css'), - bundleFile('#{app.id}.style.css'), + bundleFile('#{app.getId()}.style.css'), bundleFile('commons.bundle.js'), - bundleFile('#{app.id}.bundle.js') + bundleFile('#{app.getId()}.bundle.js') ]; (function next() { diff --git a/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js b/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js index 21dc8781f4bc1b..b35caa0e3a73d5 100644 --- a/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js +++ b/src/ui/ui_settings/__tests__/ui_settings_mixin_integration.js @@ -1,7 +1,7 @@ import sinon from 'sinon'; import expect from 'expect.js'; -import Config from '../../../server/config/config'; +import { Config } from '../../../server/config'; /* eslint-disable import/no-duplicates */ import * as uiSettingsServiceFactoryNS from '../ui_settings_service_factory'; diff --git a/src/ui/ui_settings/ui_settings_mixin.js b/src/ui/ui_settings/ui_settings_mixin.js index 7d2b7bf4397c94..1ba6ce614793e9 100644 --- a/src/ui/ui_settings/ui_settings_mixin.js +++ b/src/ui/ui_settings/ui_settings_mixin.js @@ -1,6 +1,5 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory'; import { getUiSettingsServiceForRequest } from './ui_settings_service_for_request'; -import { UiExportsConsumer } from './ui_exports_consumer'; import { deleteRoute, getRoute, @@ -9,12 +8,8 @@ import { } from './routes'; export function uiSettingsMixin(kbnServer, server) { - // reads the "uiSettingDefaults" from uiExports - const uiExportsConsumer = new UiExportsConsumer(); - kbnServer.uiExports.addConsumer(uiExportsConsumer); - const getDefaults = () => ( - uiExportsConsumer.getUiSettingDefaults() + kbnServer.uiExports.uiSettingDefaults ); server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { From f1d01c3ffae934421a770da22b49b2ad355bdb51 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 14:29:39 -0700 Subject: [PATCH 03/67] [pluginDiscovery] fully extend config before checking enabled status --- src/plugin_discovery/find_plugin_specs.js | 34 +++++++++++++++++------ 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/plugin_discovery/find_plugin_specs.js b/src/plugin_discovery/find_plugin_specs.js index 0baf7f2a1d34c9..7af62ad4fd7ebf 100644 --- a/src/plugin_discovery/find_plugin_specs.js +++ b/src/plugin_discovery/find_plugin_specs.js @@ -23,6 +23,14 @@ function defaultConfig(settings) { ); } +function waitForComplete(observable) { + return observable + // buffer all results into a single array + .toArray() + // merge the array back into the stream when complete + .mergeMap(array => array); +} + /** * Creates a collection of observables for discovering pluginSpecs * using Kibana's defaults, settings, and config service @@ -54,18 +62,28 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { deprecations.push({ spec, message }); }); - // if the pluginSpec is disabled then disable the extension - // we made to the config service - const enabled = spec.isEnabled(config); - if (!enabled) { - disableConfigExtension(spec, config); - } - return { + spec, deprecations, - enabledSpecs: enabled ? [spec] : [], }; }) + // extend the config with all plugins before determining enabled status + .let(waitForComplete) + .map(result => { + const enabled = result.spec.isEnabled(config); + return { + ...result, + enabledSpecs: enabled ? [result.spec] : [], + disabledSpecs: enabled ? [] : [result.spec] + }; + }) + // determin which plugins are disabled before actually removing things from the config + .let(waitForComplete) + .do(result => { + for (const spec of result.disabledSpecs) { + disableConfigExtension(spec, config); + } + }) .share(); return { From b53526c6d611fbf553f82a2de925e75e674361ca Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 14:44:21 -0700 Subject: [PATCH 04/67] [pluginDiscovery] limit arbitrary defaults in PluginSpec --- src/plugin_discovery/plugin_config/schema.js | 4 ++- .../plugin_config/settings.js | 4 +-- .../plugin_exports/reduce_export_specs.js | 2 +- .../plugin_spec/plugin_spec.js | 16 +++++------ src/server/plugins/lib/plugin.js | 27 +++++++++---------- 5 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/plugin_discovery/plugin_config/schema.js b/src/plugin_discovery/plugin_config/schema.js index 0a4c690fef5ac6..f6e1fe8e643b48 100644 --- a/src/plugin_discovery/plugin_config/schema.js +++ b/src/plugin_discovery/plugin_config/schema.js @@ -1,4 +1,5 @@ import Joi from 'joi'; +import { noop } from 'lodash'; const STUB_CONFIG_SCHEMA = Joi.object().keys({ enabled: Joi.valid(false) @@ -15,7 +16,8 @@ const DEFAULT_CONFIG_SCHEMA = Joi.object().keys({ * @return {Promise} */ export async function getSchema(spec) { - return await spec.getConfigSchemaProvider()(Joi) || DEFAULT_CONFIG_SCHEMA; + const provider = spec.getConfigSchemaProvider() || noop; + return (await provider(Joi)) || DEFAULT_CONFIG_SCHEMA; } export function getStubSchema() { diff --git a/src/plugin_discovery/plugin_config/settings.js b/src/plugin_discovery/plugin_config/settings.js index f052e9052d4002..58102353e3ea4f 100644 --- a/src/plugin_discovery/plugin_config/settings.js +++ b/src/plugin_discovery/plugin_config/settings.js @@ -1,10 +1,10 @@ -import { get } from 'lodash'; +import { get, noop } from 'lodash'; import * as serverConfig from '../../server/config'; import { createTransform, Deprecations } from '../../deprecation'; async function getDeprecationTransformer(spec) { - const provider = spec.getDeprecationsProvider(); + const provider = spec.getDeprecationsProvider() || noop; return createTransform(await provider(Deprecations) || []); } diff --git a/src/plugin_discovery/plugin_exports/reduce_export_specs.js b/src/plugin_discovery/plugin_exports/reduce_export_specs.js index fb214339fea50f..936c9c4daced31 100644 --- a/src/plugin_discovery/plugin_exports/reduce_export_specs.js +++ b/src/plugin_discovery/plugin_exports/reduce_export_specs.js @@ -8,7 +8,7 @@ */ export function reduceExportSpecs(pluginSpecs, reducers, defaults = {}) { return pluginSpecs.reduce((acc, pluginSpec) => { - const specsByType = pluginSpec.getExportSpecs(); + const specsByType = pluginSpec.getExportSpecs() || {}; const types = Object.keys(specsByType); return types.reduce((acc, type) => { diff --git a/src/plugin_discovery/plugin_spec/plugin_spec.js b/src/plugin_discovery/plugin_spec/plugin_spec.js index ff4cee7c4ffd9f..1f971c8f887469 100644 --- a/src/plugin_discovery/plugin_spec/plugin_spec.js +++ b/src/plugin_discovery/plugin_spec/plugin_spec.js @@ -1,7 +1,7 @@ import { resolve, basename, isAbsolute as isAbsolutePath } from 'path'; import toPath from 'lodash/internal/toPath'; -import { get, noop } from 'lodash'; +import { get } from 'lodash'; export class PluginSpec { /** @@ -71,7 +71,7 @@ export class PluginSpec { throw new TypeError('Unable to determin plugin version'); } - if (!Array.isArray(this.getRequiredPluginIds())) { + if (this.getRequiredPluginIds() !== undefined && !Array.isArray(this.getRequiredPluginIds())) { throw new TypeError('"plugin.require" must be an array of plugin ids'); } @@ -122,7 +122,7 @@ export class PluginSpec { } getRequiredPluginIds() { - return this._require || []; + return this._require; } getPublicDir() { @@ -138,15 +138,15 @@ export class PluginSpec { } getExportSpecs() { - return this._uiExports || {}; + return this._uiExports; } getPreInitHandler() { - return this._preInit || noop; + return this._preInit; } getInitHandler() { - return this._init || noop; + return this._init; } getConfigPrefix() { @@ -154,7 +154,7 @@ export class PluginSpec { } getConfigSchemaProvider() { - return this._configSchemaProvider || noop; + return this._configSchemaProvider; } readConfigValue(config, key) { @@ -162,6 +162,6 @@ export class PluginSpec { } getDeprecationsProvider() { - return this._configDeprecationsProvider || noop; + return this._configDeprecationsProvider; } } diff --git a/src/server/plugins/lib/plugin.js b/src/server/plugins/lib/plugin.js index 8fb3b387324b4e..df435b9550aa95 100644 --- a/src/server/plugins/lib/plugin.js +++ b/src/server/plugins/lib/plugin.js @@ -1,5 +1,4 @@ -import { once, noop } from 'lodash'; -import Bluebird, { attempt, fromNode } from 'bluebird'; +import { once } from 'lodash'; /** * The server plugin class, used to extend the server @@ -22,8 +21,7 @@ export class Plugin { this.path = spec.getPath(); this.id = spec.getId(); this.version = spec.getVersion(); - this.requiredIds = spec.getRequiredPluginIds(); - this.uiExportsSpecs = spec.getExportSpecs(); + this.requiredIds = spec.getRequiredPluginIds() || []; this.kibanaVersion = spec.getExpectedKibanaVersion(); this.externalPreInit = spec.getPreInitHandler(); this.externalInit = spec.getInitHandler(); @@ -36,7 +34,9 @@ export class Plugin { } async preInit() { - return await this.externalPreInit(this.kbnServer.server); + if (this.externalPreInit) { + return await this.externalPreInit(this.kbnServer.server); + } } async init() { @@ -60,25 +60,24 @@ export class Plugin { // Many of the plugins are simply adding static assets to the server and we don't need // to track their "status". Since plugins must have an init() function to even set its status // we shouldn't even create a status unless the plugin can use it. - if (this.externalInit !== noop) { + if (this.externalInit) { this.status = kbnServer.status.createForPlugin(this); server.expose('status', this.status); + await this.externalInit(server, options); } - - return await attempt(this.externalInit, [server, options], this); }; const register = (server, options, next) => { - Bluebird.resolve(asyncRegister(server, options)).nodeify(next); + asyncRegister(server, options) + .then(() => next()) + .catch(next); }; register.attributes = { name: id, version: version }; - await fromNode(cb => { - kbnServer.server.register({ - register: register, - options: config.has(configPrefix) ? config.get(configPrefix) : null - }, cb); + await kbnServer.server.register({ + register: register, + options: config.has(configPrefix) ? config.get(configPrefix) : null }); // Only change the plugin status to green if the From d97d0cc79bf3c540f2044573fcc487858c93c26d Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 14:52:42 -0700 Subject: [PATCH 05/67] [ui/navLink] fix tests --- src/ui/__tests__/ui_nav_link.js | 138 ------------------ src/ui/ui_nav_links/__tests__/ui_nav_link.js | 142 +++++++++++++++++++ 2 files changed, 142 insertions(+), 138 deletions(-) delete mode 100644 src/ui/__tests__/ui_nav_link.js create mode 100644 src/ui/ui_nav_links/__tests__/ui_nav_link.js diff --git a/src/ui/__tests__/ui_nav_link.js b/src/ui/__tests__/ui_nav_link.js deleted file mode 100644 index c8f9648e6a1c69..00000000000000 --- a/src/ui/__tests__/ui_nav_link.js +++ /dev/null @@ -1,138 +0,0 @@ -import expect from 'expect.js'; - -import UiNavLink from '../ui_nav_link'; - -describe('UiNavLink', () => { - describe('constructor', () => { - it ('initializes the object properties as expected', () => { - const uiExports = { - urlBasePath: 'http://localhost:5601/rnd' - }; - const spec = { - id: 'kibana:discover', - title: 'Discover', - order: -1003, - url: '/app/kibana#/discover', - description: 'interactively explore your data', - icon: 'plugins/kibana/assets/discover.svg', - hidden: true, - disabled: true - }; - const link = new UiNavLink(uiExports, spec); - - expect(link.id).to.be(spec.id); - expect(link.title).to.be(spec.title); - expect(link.order).to.be(spec.order); - expect(link.url).to.be(`${uiExports.urlBasePath}${spec.url}`); - expect(link.description).to.be(spec.description); - expect(link.icon).to.be(spec.icon); - expect(link.hidden).to.be(spec.hidden); - expect(link.disabled).to.be(spec.disabled); - }); - - it ('initializes the url property without a base path when one is not specified in the spec', () => { - const uiExports = {}; - const spec = { - id: 'kibana:discover', - title: 'Discover', - order: -1003, - url: '/app/kibana#/discover', - description: 'interactively explore your data', - icon: 'plugins/kibana/assets/discover.svg', - }; - const link = new UiNavLink(uiExports, spec); - - expect(link.url).to.be(spec.url); - }); - - it ('initializes the order property to 0 when order is not specified in the spec', () => { - const uiExports = {}; - const spec = { - id: 'kibana:discover', - title: 'Discover', - url: '/app/kibana#/discover', - description: 'interactively explore your data', - icon: 'plugins/kibana/assets/discover.svg', - }; - const link = new UiNavLink(uiExports, spec); - - expect(link.order).to.be(0); - }); - - it ('initializes the linkToLastSubUrl property to false when false is specified in the spec', () => { - const uiExports = {}; - const spec = { - id: 'kibana:discover', - title: 'Discover', - order: -1003, - url: '/app/kibana#/discover', - description: 'interactively explore your data', - icon: 'plugins/kibana/assets/discover.svg', - linkToLastSubUrl: false - }; - const link = new UiNavLink(uiExports, spec); - - expect(link.linkToLastSubUrl).to.be(false); - }); - - it ('initializes the linkToLastSubUrl property to true by default', () => { - const uiExports = {}; - const spec = { - id: 'kibana:discover', - title: 'Discover', - order: -1003, - url: '/app/kibana#/discover', - description: 'interactively explore your data', - icon: 'plugins/kibana/assets/discover.svg', - }; - const link = new UiNavLink(uiExports, spec); - - expect(link.linkToLastSubUrl).to.be(true); - }); - - it ('initializes the hidden property to false by default', () => { - const uiExports = {}; - const spec = { - id: 'kibana:discover', - title: 'Discover', - order: -1003, - url: '/app/kibana#/discover', - description: 'interactively explore your data', - icon: 'plugins/kibana/assets/discover.svg', - }; - const link = new UiNavLink(uiExports, spec); - - expect(link.hidden).to.be(false); - }); - - it ('initializes the disabled property to false by default', () => { - const uiExports = {}; - const spec = { - id: 'kibana:discover', - title: 'Discover', - order: -1003, - url: '/app/kibana#/discover', - description: 'interactively explore your data', - icon: 'plugins/kibana/assets/discover.svg', - }; - const link = new UiNavLink(uiExports, spec); - - expect(link.disabled).to.be(false); - }); - - it ('initializes the tooltip property to an empty string by default', () => { - const uiExports = {}; - const spec = { - id: 'kibana:discover', - title: 'Discover', - order: -1003, - url: '/app/kibana#/discover', - description: 'interactively explore your data', - icon: 'plugins/kibana/assets/discover.svg', - }; - const link = new UiNavLink(uiExports, spec); - - expect(link.tooltip).to.be(''); - }); - }); -}); diff --git a/src/ui/ui_nav_links/__tests__/ui_nav_link.js b/src/ui/ui_nav_links/__tests__/ui_nav_link.js new file mode 100644 index 00000000000000..ee3a8c6c977c96 --- /dev/null +++ b/src/ui/ui_nav_links/__tests__/ui_nav_link.js @@ -0,0 +1,142 @@ +import expect from 'expect.js'; + +import { UiNavLink } from '../ui_nav_link'; + +describe('UiNavLink', () => { + describe('constructor', () => { + it('initializes the object properties as expected', () => { + const urlBasePath = 'http://localhost:5601/rnd'; + const spec = { + id: 'kibana:discover', + title: 'Discover', + order: -1003, + url: '/app/kibana#/discover', + description: 'interactively explore your data', + icon: 'plugins/kibana/assets/discover.svg', + hidden: true, + disabled: true + }; + + const link = new UiNavLink(urlBasePath, spec); + expect(link.toJSON()).to.eql({ + id: spec.id, + title: spec.title, + order: spec.order, + url: `${urlBasePath}${spec.url}`, + subUrlBase: `${urlBasePath}${spec.url}`, + description: spec.description, + icon: spec.icon, + hidden: spec.hidden, + disabled: spec.disabled, + + // defaults + linkToLastSubUrl: true, + tooltip: '' + }); + }); + + it('initializes the url property without a base path when one is not specified in the spec', () => { + const urlBasePath = undefined; + const spec = { + id: 'kibana:discover', + title: 'Discover', + order: -1003, + url: '/app/kibana#/discover', + description: 'interactively explore your data', + icon: 'plugins/kibana/assets/discover.svg', + }; + const link = new UiNavLink(urlBasePath, spec); + expect(link.toJSON()).to.have.property('url', spec.url); + }); + + it('initializes the order property to 0 when order is not specified in the spec', () => { + const urlBasePath = undefined; + const spec = { + id: 'kibana:discover', + title: 'Discover', + url: '/app/kibana#/discover', + description: 'interactively explore your data', + icon: 'plugins/kibana/assets/discover.svg', + }; + const link = new UiNavLink(urlBasePath, spec); + + expect(link.toJSON()).to.have.property('order', 0); + }); + + it('initializes the linkToLastSubUrl property to false when false is specified in the spec', () => { + const urlBasePath = undefined; + const spec = { + id: 'kibana:discover', + title: 'Discover', + order: -1003, + url: '/app/kibana#/discover', + description: 'interactively explore your data', + icon: 'plugins/kibana/assets/discover.svg', + linkToLastSubUrl: false + }; + const link = new UiNavLink(urlBasePath, spec); + + expect(link.toJSON()).to.have.property('linkToLastSubUrl', false); + }); + + it('initializes the linkToLastSubUrl property to true by default', () => { + const urlBasePath = undefined; + const spec = { + id: 'kibana:discover', + title: 'Discover', + order: -1003, + url: '/app/kibana#/discover', + description: 'interactively explore your data', + icon: 'plugins/kibana/assets/discover.svg', + }; + const link = new UiNavLink(urlBasePath, spec); + + expect(link.toJSON()).to.have.property('linkToLastSubUrl', true); + }); + + it('initializes the hidden property to false by default', () => { + const urlBasePath = undefined; + const spec = { + id: 'kibana:discover', + title: 'Discover', + order: -1003, + url: '/app/kibana#/discover', + description: 'interactively explore your data', + icon: 'plugins/kibana/assets/discover.svg', + }; + const link = new UiNavLink(urlBasePath, spec); + + expect(link.toJSON()).to.have.property('hidden', false); + }); + + it('initializes the disabled property to false by default', () => { + const urlBasePath = undefined; + const spec = { + id: 'kibana:discover', + title: 'Discover', + order: -1003, + url: '/app/kibana#/discover', + description: 'interactively explore your data', + icon: 'plugins/kibana/assets/discover.svg', + }; + const link = new UiNavLink(urlBasePath, spec); + + expect(link.toJSON()).to.have.property('disabled', false); + }); + + it('initializes the tooltip property to an empty string by default', () => { + const urlBasePath = undefined; + const spec = { + id: 'kibana:discover', + title: 'Discover', + order: -1003, + url: '/app/kibana#/discover', + description: 'interactively explore your data', + icon: 'plugins/kibana/assets/discover.svg', + }; + const link = new UiNavLink(urlBasePath, spec); + + expect(link.toJSON()).to.have.property('tooltip', ''); + }); + }); +}); From 76f81286538549d7c1bd8c766e1db88a6c76156b Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 15:02:27 -0700 Subject: [PATCH 06/67] [ui/injectedVars] fix tests --- src/ui/__tests__/fixtures/test_app/index.js | 6 + src/ui/__tests__/ui_exports.js | 107 ------------------ .../ui_exports_replace_injected_vars.js | 7 +- src/ui/ui_render/ui_render_mixin.js | 31 ++--- 4 files changed, 23 insertions(+), 128 deletions(-) delete mode 100644 src/ui/__tests__/ui_exports.js diff --git a/src/ui/__tests__/fixtures/test_app/index.js b/src/ui/__tests__/fixtures/test_app/index.js index 05db665baaf2e1..129804e69e4908 100644 --- a/src/ui/__tests__/fixtures/test_app/index.js +++ b/src/ui/__tests__/fixtures/test_app/index.js @@ -8,6 +8,12 @@ export default kibana => new kibana.Plugin({ from_test_app: true }; } + }, + + injectDefaultVars() { + return { + from_defaults: true + }; } } }); diff --git a/src/ui/__tests__/ui_exports.js b/src/ui/__tests__/ui_exports.js deleted file mode 100644 index 71ed5896f28df5..00000000000000 --- a/src/ui/__tests__/ui_exports.js +++ /dev/null @@ -1,107 +0,0 @@ -import expect from 'expect.js'; -import { resolve } from 'path'; - -import UiExports from '../ui_exports'; -import * as kbnTestServer from '../../test_utils/kbn_server'; - -describe('UiExports', function () { - describe('#find()', function () { - it('finds exports based on the passed export names', function () { - const uiExports = new UiExports({}); - uiExports.aliases.foo = ['a', 'b', 'c']; - uiExports.aliases.bar = ['d', 'e', 'f']; - - expect(uiExports.find(['foo'])).to.eql(['a', 'b', 'c']); - expect(uiExports.find(['bar'])).to.eql(['d', 'e', 'f']); - expect(uiExports.find(['foo', 'bar'])).to.eql(['a', 'b', 'c', 'd', 'e', 'f']); - }); - - it('allows query types that match nothing', function () { - const uiExports = new UiExports({}); - uiExports.aliases.foo = ['a', 'b', 'c']; - - expect(uiExports.find(['foo'])).to.eql(['a', 'b', 'c']); - expect(uiExports.find(['bar'])).to.eql([]); - expect(uiExports.find(['foo', 'bar'])).to.eql(['a', 'b', 'c']); - }); - }); -// - describe('#defaultInjectedVars', function () { - describe('two plugins, two sync', function () { - this.slow(10000); - this.timeout(60000); - - let kbnServer; - before(async function () { - kbnServer = kbnTestServer.createServer({ - plugins: { - paths: [ - resolve(__dirname, 'fixtures/plugin_foo'), - resolve(__dirname, 'fixtures/plugin_bar'), - ] - }, - - plugin_foo: { - shared: 'foo' - }, - - plugin_bar: { - shared: 'bar' - } - }); - - await kbnServer.ready(); - }); - - after(async function () { - await kbnServer.close(); - }); - - it('merges the two plugins in the order they are loaded', function () { - expect(kbnServer.uiExports.defaultInjectedVars).to.eql({ - shared: 'foo' - }); - }); - }); - - describe('two plugins, one async', function () { - this.slow(10000); - this.timeout(60000); - - let kbnServer; - before(async function () { - kbnServer = kbnTestServer.createServer({ - plugins: { - scanDirs: [], - paths: [ - resolve(__dirname, 'fixtures/plugin_async_foo'), - resolve(__dirname, 'fixtures/plugin_bar'), - ] - }, - - plugin_async_foo: { - delay: 500, - shared: 'foo' - }, - - plugin_bar: { - shared: 'bar' - } - }); - - await kbnServer.ready(); - }); - - after(async function () { - await kbnServer.close(); - }); - - it('merges the two plugins in the order they are loaded', function () { - // even though plugin_async_foo loads 500ms later, it is still "first" to merge - expect(kbnServer.uiExports.defaultInjectedVars).to.eql({ - shared: 'foo' - }); - }); - }); - }); -}); diff --git a/src/ui/__tests__/ui_exports_replace_injected_vars.js b/src/ui/__tests__/ui_exports_replace_injected_vars.js index 3e69463251e2c0..ab751411e7b995 100644 --- a/src/ui/__tests__/ui_exports_replace_injected_vars.js +++ b/src/ui/__tests__/ui_exports_replace_injected_vars.js @@ -18,7 +18,11 @@ const injectReplacer = (kbnServer, replacer) => { // normally the replacer would be defined in a plugin's uiExports, // but that requires stubbing out an entire plugin directory for // each test, so we fake it and jam the replacer into uiExports - kbnServer.uiExports.injectedVarsReplacers.push(replacer); + const { injectedVarsReplacers = [] } = kbnServer.uiExports; + kbnServer.uiExports.injectedVarsReplacers = [ + ...injectedVarsReplacers, + replacer + ]; }; describe('UiExports', function () { @@ -122,7 +126,6 @@ describe('UiExports', function () { it('starts off with the injected vars for the app merged with the default injected vars', async () => { const stub = sinon.stub(); injectReplacer(kbnServer, stub); - kbnServer.uiExports.defaultInjectedVars.from_defaults = true; await kbnServer.inject('/app/test_app'); sinon.assert.calledOnce(stub); diff --git a/src/ui/ui_render/ui_render_mixin.js b/src/ui/ui_render/ui_render_mixin.js index 4a97cc48f7cbaf..42c0c066fc7476 100644 --- a/src/ui/ui_render/ui_render_mixin.js +++ b/src/ui/ui_render/ui_render_mixin.js @@ -3,35 +3,28 @@ import { props, reduce as reduceAsync } from 'bluebird'; import Boom from 'boom'; import { resolve } from 'path'; -function getDefaultInjectedVars(kbnServer) { - const { defaultInjectedVarProviders = [] } = kbnServer.uiExports; - - return defaultInjectedVarProviders.reduce((allDefaults, { fn, pluginSpec }) => ( - defaults( - allDefaults, - fn(kbnServer.server, pluginSpec.readConfigValue(kbnServer.config, [])) - ) - ), {}); -} +export function uiRenderMixin(kbnServer, server, config) { -function getReplaceInjectedVars(kbnServer) { - const { injectedVarsReplacers = [] } = kbnServer.uiExports; + function replaceInjectedVars(request, injectedVars) { + const { injectedVarsReplacers = [] } = kbnServer.uiExports; - return function replaceInjectedVars(request, injectedVars) { return reduceAsync( injectedVarsReplacers, async (acc, replacer) => await replacer(acc, request, kbnServer.server), injectedVars ); - }; -} - -export function uiRenderMixin(kbnServer, server, config) { - const replaceInjectedVars = getReplaceInjectedVars(kbnServer); + } let defaultInjectedVars = {}; kbnServer.afterPluginsInit(() => { - defaultInjectedVars = getDefaultInjectedVars(kbnServer); + const { defaultInjectedVarProviders = [] } = kbnServer.uiExports; + defaultInjectedVars = defaultInjectedVarProviders + .reduce((allDefaults, { fn, pluginSpec }) => ( + defaults( + allDefaults, + fn(kbnServer.server, pluginSpec.readConfigValue(kbnServer.config, [])) + ) + ), {}); }); // render all views from ./views From d77ced8533ce322cd3f69cf6aa6c8ac43a2ba4d3 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 15:36:22 -0700 Subject: [PATCH 07/67] [ui/app] fix tests --- src/ui/__tests__/ui_app.js | 278 ----------------------------- src/ui/ui_apps/__tests__/ui_app.js | 269 ++++++++++++++++++++++++++++ src/ui/ui_apps/ui_app.js | 19 +- 3 files changed, 281 insertions(+), 285 deletions(-) delete mode 100644 src/ui/__tests__/ui_app.js create mode 100644 src/ui/ui_apps/__tests__/ui_app.js diff --git a/src/ui/__tests__/ui_app.js b/src/ui/__tests__/ui_app.js deleted file mode 100644 index d7a8889e8afba0..00000000000000 --- a/src/ui/__tests__/ui_app.js +++ /dev/null @@ -1,278 +0,0 @@ -import expect from 'expect.js'; -import UiApp from '../ui_app.js'; -import UiExports from '../ui_exports'; -import { noop } from 'lodash'; - -function getMockSpec(extraParams) { - return { - id: 'uiapp-test', - main: 'main.js', - title: 'UIApp Test', - order: 9000, - description: 'Test of UI App Constructor', - icon: 'ui_app_test.svg', - linkToLastSubUrl: true, - hidden: false, - listed: null, - templateName: 'ui_app_test', - ...extraParams - }; -} -describe('UiApp', () => { - describe('constructor', () => { - const uiExports = new UiExports({}); - - it('throws an exception if an ID is not given', () => { - function newAppMissingID() { - const spec = {}; // should have id property - const newApp = new UiApp(uiExports, spec); - return newApp; - } - expect(newAppMissingID).to.throwException(); - }); - - describe('defaults', () => { - const spec = { id: 'uiapp-test-defaults' }; - let newApp; - beforeEach(() => { - newApp = new UiApp(uiExports, spec); - }); - - it('copies the ID from the spec', () => { - expect(newApp.id).to.be(spec.id); - }); - - it('has a default navLink', () => { - expect(newApp.navLink).to.eql({ - id: 'uiapp-test-defaults', - title: undefined, - order: 0, - url: '/app/uiapp-test-defaults', - subUrlBase: '/app/uiapp-test-defaults', - description: undefined, - icon: undefined, - linkToLastSubUrl: true, - hidden: false, - disabled: false, - tooltip: '' - }); - }); - - it('has a default order of 0', () => { - expect(newApp.order).to.be(0); - }); - - it('has a default template name of ui_app', () => { - expect(newApp.templateName).to.be('ui_app'); - }); - }); - - describe('with spec', () => { - const spec = getMockSpec(); - let newApp; - beforeEach(() => { - newApp = new UiApp(uiExports, spec); - }); - - it('copies the ID from the spec', () => { - expect(newApp.id).to.be(spec.id); - }); - - it('copies field values from spec', () => { - // test that the fields exist, but have undefined value - expect(newApp.main).to.be(spec.main); - expect(newApp.title).to.be(spec.title); - expect(newApp.description).to.be(spec.description); - expect(newApp.icon).to.be(spec.icon); - expect(newApp.linkToLastSubUrl).to.be(spec.linkToLastSubUrl); - expect(newApp.templateName).to.be(spec.templateName); - expect(newApp.order).to.be(spec.order); - expect(newApp.navLink).to.eql({ - id: 'uiapp-test', - title: 'UIApp Test', - order: 9000, - url: '/app/uiapp-test', - subUrlBase: '/app/uiapp-test', - description: 'Test of UI App Constructor', - icon: 'ui_app_test.svg', - linkToLastSubUrl: true, - hidden: false, - disabled: false, - tooltip: '' - }); - }); - }); - - describe('reference fields', () => { - const spec = getMockSpec({ testSpec: true }); - let newApp; - beforeEach(() => { - newApp = new UiApp(uiExports, spec); - }); - - it('has a reference to the uiExports object', () => { - expect(newApp.uiExports).to.be(uiExports); - }); - - it('has a reference to the original spec', () => { - expect(newApp.spec).to.be(spec); - }); - - it('has a reference to the spec.injectVars function', () => { - const helloFunction = () => 'hello'; - const spec = { - id: 'uiapp-test', - injectVars: helloFunction - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.getInjectedVars).to.be(helloFunction); - }); - }); - - describe('app.getInjectedVars', () => { - it('is noop function by default', () => { - const spec = { - id: 'uiapp-test' - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.getInjectedVars).to.be(noop); - }); - }); - - /* - * The "hidden" and "listed" flags have an bound relationship. The "hidden" - * flag gets cast to a boolean value, and the "listed" flag is dependent on - * "hidden" - */ - describe('hidden flag', () => { - describe('is cast to boolean value', () => { - it('when undefined', () => { - const spec = { - id: 'uiapp-test', - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.hidden).to.be(false); - }); - - it('when null', () => { - const spec = { - id: 'uiapp-test', - hidden: null, - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.hidden).to.be(false); - }); - }); - }); - - describe('listed flag', () => { - describe('defaults to the opposite value of hidden', () => { - it(`when it's null and hidden is true`, () => { - const spec = { - id: 'uiapp-test', - hidden: true, - listed: null, - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.listed).to.be(false); - }); - - it(`when it's null and hidden is false`, () => { - const spec = { - id: 'uiapp-test', - hidden: false, - listed: null, - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.listed).to.be(true); - }); - - it(`when it's undefined and hidden is false`, () => { - const spec = { - id: 'uiapp-test', - hidden: false, - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.listed).to.be(true); - }); - - it(`when it's undefined and hidden is true`, () => { - const spec = { - id: 'uiapp-test', - hidden: true, - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.listed).to.be(false); - }); - }); - - it(`is set to true when it's passed as true`, () => { - const spec = { - id: 'uiapp-test', - listed: true, - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.listed).to.be(true); - }); - - it(`is set to false when it's passed as false`, () => { - const spec = { - id: 'uiapp-test', - listed: false, - }; - const newApp = new UiApp(uiExports, spec); - expect(newApp.listed).to.be(false); - }); - }); - }); - - describe('getModules', () => { - it('gets modules from uiExports', () => { - const uiExports = new UiExports({}); - uiExports.consumePlugin({ - uiExportsSpecs: { - chromeNavControls: [ 'plugins/ui_app_test/views/nav_control' ], - hacks: [ 'plugins/ui_app_test/hacks/init' ] - } - }); - const spec = getMockSpec(); - const newApp = new UiApp(uiExports, spec); - - expect(newApp.getModules()).to.eql([ - 'main.js', - 'plugins/ui_app_test/views/nav_control', - 'plugins/ui_app_test/hacks/init' - ]); - }); - }); - - describe('toJSON', function () { - it('creates plain object', () => { - const uiExports = new UiExports({}); - const spec = getMockSpec(); - const newApp = new UiApp(uiExports, spec); - - expect(newApp.toJSON()).to.eql({ - id: 'uiapp-test', - title: 'UIApp Test', - description: 'Test of UI App Constructor', - icon: 'ui_app_test.svg', - main: 'main.js', - navLink: { - id: 'uiapp-test', - title: 'UIApp Test', - order: 9000, - url: '/app/uiapp-test', - subUrlBase: '/app/uiapp-test', - description: 'Test of UI App Constructor', - icon: 'ui_app_test.svg', - linkToLastSubUrl: true, - hidden: false, - disabled: false, - tooltip: '' - }, - linkToLastSubUrl: true - }); - }); - }); -}); diff --git a/src/ui/ui_apps/__tests__/ui_app.js b/src/ui/ui_apps/__tests__/ui_app.js new file mode 100644 index 00000000000000..7bc05a8ce0a534 --- /dev/null +++ b/src/ui/ui_apps/__tests__/ui_app.js @@ -0,0 +1,269 @@ +import sinon from 'sinon'; +import expect from 'expect.js'; + +import { UiApp } from '../ui_app'; + +function getMockSpec(extraParams) { + return { + id: 'uiapp-test', + main: 'main.js', + title: 'UIApp Test', + order: 9000, + description: 'Test of UI App Constructor', + icon: 'ui_app_test.svg', + linkToLastSubUrl: true, + hidden: false, + listed: null, + templateName: 'ui_app_test', + ...extraParams + }; +} + +function getMockKbnServer() { + return { + plugins: [], + uiExports: {}, + config: { + get: sinon.stub() + .withArgs('server.basePath') + .returns('') + } + }; +} + +describe('UiApp', () => { + describe('constructor', () => { + it('throws an exception if an ID is not given', () => { + function newAppMissingID() { + const spec = {}; // should have id property + const kbnServer = getMockKbnServer(); + const newApp = new UiApp(kbnServer, spec); + return newApp; + } + expect(newAppMissingID).to.throwException(); + }); + + describe('defaults', () => { + const spec = { id: 'uiapp-test-defaults' }; + const kbnServer = getMockKbnServer(); + let newApp; + beforeEach(() => { + newApp = new UiApp(kbnServer, spec); + }); + + it('has the ID from the spec', () => { + expect(newApp.getId()).to.be(spec.id); + }); + + it('has a navLink', () => { + expect(!!newApp.getNavLink()).to.be(true); + }); + + it('has a default template name of ui_app', () => { + expect(newApp.getTemplateName()).to.be('ui_app'); + }); + + describe('uiApp.getInjectedVars()', () => { + it('returns undefined by default', () => { + expect(newApp.getInjectedVars()).to.be(undefined); + }); + }); + + describe('JSON representation', () => { + it('has defaults', () => { + expect(JSON.parse(JSON.stringify(newApp))).to.eql({ + id: spec.id, + navLink: { + id: 'uiapp-test-defaults', + order: 0, + url: '/app/uiapp-test-defaults', + subUrlBase: '/app/uiapp-test-defaults', + linkToLastSubUrl: true, + hidden: false, + disabled: false, + tooltip: '' + }, + }); + }); + }); + }); + + describe('mock spec', () => { + describe('JSON representation', () => { + it('has defaults and values from spec', () => { + const kbnServer = getMockKbnServer(); + const spec = getMockSpec(); + const uiApp = new UiApp(kbnServer, spec); + + expect(JSON.parse(JSON.stringify(uiApp))).to.eql({ + id: spec.id, + title: spec.title, + description: spec.description, + icon: spec.icon, + main: spec.main, + linkToLastSubUrl: spec.linkToLastSubUrl, + navLink: { + id: 'uiapp-test', + title: 'UIApp Test', + order: 9000, + url: '/app/uiapp-test', + subUrlBase: '/app/uiapp-test', + description: 'Test of UI App Constructor', + icon: 'ui_app_test.svg', + linkToLastSubUrl: true, + hidden: false, + disabled: false, + tooltip: '' + }, + }); + }); + }); + }); + + /* + * The "hidden" and "listed" flags have an bound relationship. The "hidden" + * flag gets cast to a boolean value, and the "listed" flag is dependent on + * "hidden" + */ + describe('hidden flag', () => { + describe('is cast to boolean value', () => { + it('when undefined', () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isHidden()).to.be(false); + }); + + it('when null', () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + hidden: null, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isHidden()).to.be(false); + }); + + it('when 0', () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + hidden: 0, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isHidden()).to.be(false); + }); + + it('when true', () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + hidden: true, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isHidden()).to.be(true); + }); + + it('when 1', () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + hidden: 1, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isHidden()).to.be(true); + }); + }); + }); + + describe('listed flag', () => { + describe('defaults to the opposite value of hidden', () => { + it(`when it's null and hidden is true`, () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + hidden: true, + listed: null, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isListed()).to.be(false); + }); + + it(`when it's null and hidden is false`, () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + hidden: false, + listed: null, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isListed()).to.be(true); + }); + + it(`when it's undefined and hidden is false`, () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + hidden: false, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isListed()).to.be(true); + }); + + it(`when it's undefined and hidden is true`, () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + hidden: true, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isListed()).to.be(false); + }); + }); + + it(`is set to true when it's passed as true`, () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + listed: true, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isListed()).to.be(true); + }); + + it(`is set to false when it's passed as false`, () => { + const kbnServer = getMockKbnServer(); + const spec = { + id: 'uiapp-test', + listed: false, + }; + const newApp = new UiApp(kbnServer, spec); + expect(newApp.isListed()).to.be(false); + }); + }); + }); + + describe('getModules', () => { + it('gets modules from kbnServer', () => { + const spec = getMockSpec(); + const kbnServer = { + ...getMockKbnServer(), + uiExports: { + appExtensions: { + chromeNavControls: [ 'plugins/ui_app_test/views/nav_control' ], + hacks: [ 'plugins/ui_app_test/hacks/init' ] + } + } + }; + + const newApp = new UiApp(kbnServer, spec); + expect(newApp.getModules()).to.eql([ + 'main.js', + 'plugins/ui_app_test/views/nav_control', + 'plugins/ui_app_test/hacks/init' + ]); + }); + }); +}); diff --git a/src/ui/ui_apps/ui_app.js b/src/ui/ui_apps/ui_app.js index 157ade46516fc6..eaad8671e4a0e1 100644 --- a/src/ui/ui_apps/ui_app.js +++ b/src/ui/ui_apps/ui_app.js @@ -1,5 +1,3 @@ -import { noop } from 'lodash'; - import { UiNavLink } from '../ui_nav_links'; export class UiApp { @@ -14,9 +12,9 @@ export class UiApp { icon, hidden, linkToLastSubUrl, - listed = !hidden, + listed, templateName = 'ui_app', - injectVars = noop, + injectVars, url = `/app/${id}`, uses = [] } = spec; @@ -40,7 +38,7 @@ export class UiApp { this._icon = icon; this._linkToLastSubUrl = linkToLastSubUrl; this._hidden = hidden; - this._listed = !hidden && listed; + this._listed = listed; this._templateName = templateName; this._url = url; this._pluginId = pluginId; @@ -70,7 +68,7 @@ export class UiApp { // unless an app is hidden it gets a navlink, but we only respond to `getNavLink()` // if the app is also listed. This means that all apps in the kibanaPayload will // have a navLink property since that list includes all normally accessible apps - this._navLink = new UiNavLink(kbnServer, { + this._navLink = new UiNavLink(kbnServer.config.get('server.basePath'), { id: this._id, title: this._title, order: this._order, @@ -98,8 +96,15 @@ export class UiApp { return !!this._hidden; } + isListed() { + return ( + !this.isHidden() && + (this._listed == null || !!this._listed) + ); + } + getNavLink() { - if (this._listed) { + if (this.isListed()) { return this._navLink; } } From 15f0a3063d25b7a988fcc54e4b83c5a764b0d406 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 15:47:06 -0700 Subject: [PATCH 08/67] [server/plugins] convert init to callPluginHook tests --- src/server/plugins/__tests__/plugin_init.js | 77 ---------------- .../plugins/lib/__tests__/call_plugin_hook.js | 87 +++++++++++++++++++ 2 files changed, 87 insertions(+), 77 deletions(-) delete mode 100644 src/server/plugins/__tests__/plugin_init.js create mode 100644 src/server/plugins/lib/__tests__/call_plugin_hook.js diff --git a/src/server/plugins/__tests__/plugin_init.js b/src/server/plugins/__tests__/plugin_init.js deleted file mode 100644 index 189a99067b7110..00000000000000 --- a/src/server/plugins/__tests__/plugin_init.js +++ /dev/null @@ -1,77 +0,0 @@ -import { values } from 'lodash'; -import expect from 'expect.js'; -import sinon from 'sinon'; -import pluginInit from '../plugin_init'; - -describe('Plugin init', () => { - const getPluginCollection = (plugins) => ({ - byId: plugins, - toArray: () => values(plugins) - }); - - it('should call preInit before init', async () => { - const plugins = { - foo: { - id: 'foo', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: [] - }, - bar: { - id: 'bar', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: [] - }, - baz: { - id: 'baz', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: [] - } - }; - - await pluginInit(getPluginCollection(plugins)); - - expect(plugins.foo.preInit.calledBefore(plugins.foo.init)).to.be.ok(); - expect(plugins.foo.preInit.calledBefore(plugins.bar.init)).to.be.ok(); - expect(plugins.foo.preInit.calledBefore(plugins.baz.init)).to.be.ok(); - - expect(plugins.bar.preInit.calledBefore(plugins.foo.init)).to.be.ok(); - expect(plugins.bar.preInit.calledBefore(plugins.bar.init)).to.be.ok(); - expect(plugins.bar.preInit.calledBefore(plugins.baz.init)).to.be.ok(); - - expect(plugins.baz.preInit.calledBefore(plugins.foo.init)).to.be.ok(); - expect(plugins.baz.preInit.calledBefore(plugins.bar.init)).to.be.ok(); - expect(plugins.baz.preInit.calledBefore(plugins.baz.init)).to.be.ok(); - }); - - it('should call preInits in correct order based on requirements', async () => { - const plugins = { - foo: { - id: 'foo', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar', 'baz'] - }, - bar: { - id: 'bar', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: [] - }, - baz: { - id: 'baz', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar'] - } - }; - - await pluginInit(getPluginCollection(plugins)); - - expect(plugins.bar.preInit.firstCall.calledBefore(plugins.foo.init.firstCall)).to.be.ok(); - expect(plugins.bar.preInit.firstCall.calledBefore(plugins.baz.init.firstCall)).to.be.ok(); - expect(plugins.baz.preInit.firstCall.calledBefore(plugins.foo.init.firstCall)).to.be.ok(); - }); -}); diff --git a/src/server/plugins/lib/__tests__/call_plugin_hook.js b/src/server/plugins/lib/__tests__/call_plugin_hook.js new file mode 100644 index 00000000000000..4c6ec852e2d55f --- /dev/null +++ b/src/server/plugins/lib/__tests__/call_plugin_hook.js @@ -0,0 +1,87 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import { callPluginHook } from '../call_plugin_hook'; + +describe('server/plugins/callPluginHook', () => { + it('should call in correct order based on requirements', async () => { + const plugins = [ + { + id: 'foo', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: ['bar', 'baz'] + }, + { + id: 'bar', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: [] + }, + { + id: 'baz', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: ['bar'] + } + ]; + + await callPluginHook('init', plugins, 'foo', []); + const [foo, bar, baz] = plugins; + sinon.assert.calledOnce(foo.init); + sinon.assert.calledTwice(bar.init); + sinon.assert.calledOnce(baz.init); + sinon.assert.callOrder( + bar.init, + baz.init, + foo.init, + ); + }); + + it('throws meaningful error when required plugin is missing', async () => { + const plugins = [ + { + id: 'foo', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: ['bar'] + }, + ]; + + try { + await callPluginHook('init', plugins, 'foo', []); + throw new Error('expected callPluginHook to throw'); + } catch (error) { + expect(error.message).to.contain('"bar" for plugin "foo"'); + } + }); + + it('throws meaningful error when dependencies are circular', async () => { + const plugins = [ + { + id: 'foo', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: ['bar'] + }, + { + id: 'bar', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: ['baz'] + }, + { + id: 'baz', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: ['foo'] + }, + ]; + + try { + await callPluginHook('init', plugins, 'foo', []); + throw new Error('expected callPluginHook to throw'); + } catch (error) { + expect(error.message).to.contain('foo -> bar -> baz -> foo'); + } + }); +}); From a0543290a09b8963e778b5cf959798b57f370f9a Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 16:38:55 -0700 Subject: [PATCH 09/67] [build/verifyTranslations] update verify logic --- .../translations/test_plugin_1/de.json | 4 - .../translations/test_plugin_1/en.json | 6 - .../translations/test_plugin_1/es-ES.json | 3 - .../translations/test_plugin_2/de.json | 3 - .../translations/test_plugin_2/en.json | 6 - src/ui/i18n/__tests__/i18n.js | 226 ------------------ src/ui/i18n/i18n.js | 136 ----------- src/ui/i18n/index.js | 1 - src/ui/ui_i18n/i18n.js | 9 + src/ui/ui_render/views/ui_app.jade | 2 +- tasks/build/verify_translations.js | 115 ++++----- 11 files changed, 62 insertions(+), 449 deletions(-) delete mode 100644 src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/de.json delete mode 100644 src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/en.json delete mode 100644 src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/es-ES.json delete mode 100644 src/ui/i18n/__tests__/fixtures/translations/test_plugin_2/de.json delete mode 100644 src/ui/i18n/__tests__/fixtures/translations/test_plugin_2/en.json delete mode 100644 src/ui/i18n/__tests__/i18n.js delete mode 100644 src/ui/i18n/i18n.js delete mode 100644 src/ui/i18n/index.js diff --git a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/de.json b/src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/de.json deleted file mode 100644 index ab9171f3a86cf1..00000000000000 --- a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/de.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "test_plugin_1-NO_SSL": "Dont run the DE dev server using HTTPS", - "test_plugin_1-DEV": "Run the DE server with development mode defaults" -} diff --git a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/en.json b/src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/en.json deleted file mode 100644 index 53dddcb859f709..00000000000000 --- a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "test_plugin_1-NO_SSL": "Dont run the dev server using HTTPS", - "test_plugin_1-DEV": "Run the server with development mode defaults", - "test_plugin_1-NO_RUN_SERVER": "Dont run the dev server", - "test_plugin_1-HOME": "Run along home now!" -} diff --git a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/es-ES.json b/src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/es-ES.json deleted file mode 100644 index 4a7ce753d9354f..00000000000000 --- a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_1/es-ES.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "test_plugin_1-NO_SSL": "Dont run the es-ES dev server using HTTPS! I am regsitered afterwards!" -} diff --git a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_2/de.json b/src/ui/i18n/__tests__/fixtures/translations/test_plugin_2/de.json deleted file mode 100644 index d87b0a4f3a88ce..00000000000000 --- a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_2/de.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "test_plugin_1-NO_SSL": "Dont run the DE dev server using HTTPS! I am regsitered afterwards!" -} diff --git a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_2/en.json b/src/ui/i18n/__tests__/fixtures/translations/test_plugin_2/en.json deleted file mode 100644 index 3e791c7d6e7764..00000000000000 --- a/src/ui/i18n/__tests__/fixtures/translations/test_plugin_2/en.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "test_plugin_2-XXXXXX": "This is XXXXXX string", - "test_plugin_2-YYYY_PPPP": "This is YYYY_PPPP string", - "test_plugin_2-FFFFFFFFFFFF": "This is FFFFFFFFFFFF string", - "test_plugin_2-ZZZ": "This is ZZZ string" -} diff --git a/src/ui/i18n/__tests__/i18n.js b/src/ui/i18n/__tests__/i18n.js deleted file mode 100644 index f903f0dbd9a4d9..00000000000000 --- a/src/ui/i18n/__tests__/i18n.js +++ /dev/null @@ -1,226 +0,0 @@ -import expect from 'expect.js'; -import _ from 'lodash'; -import { join } from 'path'; - -import { I18n } from '../'; - -const FIXTURES = join(__dirname, 'fixtures'); - -describe('ui/i18n module', function () { - - describe('one plugin', function () { - - const i18nObj = new I18n(); - - before('registerTranslations - one plugin', function () { - const pluginName = 'test_plugin_1'; - const pluginTranslationPath = join(FIXTURES, 'translations', pluginName); - const translationFiles = [ - join(pluginTranslationPath, 'de.json'), - join(pluginTranslationPath, 'en.json') - ]; - const filesLen = translationFiles.length; - for (let indx = 0; indx < filesLen; indx++) { - i18nObj.registerTranslations(translationFiles[indx]); - } - }); - - describe('getTranslations', function () { - - it('should return the translations for en locale as registered', function () { - const languageTag = ['en']; - const expectedTranslationJson = { - 'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS', - 'test_plugin_1-DEV': 'Run the server with development mode defaults', - 'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server', - 'test_plugin_1-HOME': 'Run along home now!' - }; - return checkTranslations(expectedTranslationJson, languageTag, i18nObj); - }); - - it('should return the translations for de locale as registered', function () { - const languageTag = ['de']; - const expectedTranslationJson = { - 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', - 'test_plugin_1-DEV': 'Run the DE server with development mode defaults' - }; - return checkTranslations(expectedTranslationJson, languageTag, i18nObj); - }); - - it('should pick the highest priority language for which translations exist', function () { - const languageTags = ['es-ES', 'de', 'en']; - const expectedTranslations = { - 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', - 'test_plugin_1-DEV': 'Run the DE server with development mode defaults', - }; - return checkTranslations(expectedTranslations, languageTags, i18nObj); - }); - - it('should return translations for highest priority locale where best case match is chosen from registered locales', function () { - const languageTags = ['es', 'de']; - const expectedTranslations = { - 'test_plugin_1-NO_SSL': 'Dont run the es-ES dev server using HTTPS! I am regsitered afterwards!' - }; - i18nObj.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_1', 'es-ES.json')); - return checkTranslations(expectedTranslations, languageTags, i18nObj); - }); - - it('should return an empty object for locales with no translations', function () { - const languageTags = ['ja-JA', 'fr']; - return checkTranslations({}, languageTags, i18nObj); - }); - - }); - - describe('getTranslationsForDefaultLocale', function () { - - it('should return translations for default locale which is set to the en locale', function () { - const i18nObj1 = new I18n('en'); - const expectedTranslations = { - 'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS', - 'test_plugin_1-DEV': 'Run the server with development mode defaults', - 'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server', - 'test_plugin_1-HOME': 'Run along home now!' - }; - i18nObj1.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_1', 'en.json')); - return checkTranslationsForDefaultLocale(expectedTranslations, i18nObj1); - }); - - it('should return translations for default locale which is set to the de locale', function () { - const i18nObj1 = new I18n('de'); - const expectedTranslations = { - 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', - 'test_plugin_1-DEV': 'Run the DE server with development mode defaults', - }; - i18nObj1.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_1', 'de.json')); - return checkTranslationsForDefaultLocale(expectedTranslations, i18nObj1); - }); - - }); - - describe('getAllTranslations', function () { - - it('should return all translations', function () { - const expectedTranslations = { - de: { - 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', - 'test_plugin_1-DEV': 'Run the DE server with development mode defaults' - }, - en: { - 'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS', - 'test_plugin_1-DEV': 'Run the server with development mode defaults', - 'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server', - 'test_plugin_1-HOME': 'Run along home now!' - }, - 'es-ES': { - 'test_plugin_1-NO_SSL': 'Dont run the es-ES dev server using HTTPS! I am regsitered afterwards!' - } - }; - return checkAllTranslations(expectedTranslations, i18nObj); - }); - - }); - - }); - - describe('multiple plugins', function () { - - const i18nObj = new I18n(); - - beforeEach('registerTranslations - multiple plugin', function () { - const pluginTranslationPath = join(FIXTURES, 'translations'); - const translationFiles = [ - join(pluginTranslationPath, 'test_plugin_1', 'de.json'), - join(pluginTranslationPath, 'test_plugin_1', 'en.json'), - join(pluginTranslationPath, 'test_plugin_2', 'en.json') - ]; - const filesLen = translationFiles.length; - for (let indx = 0; indx < filesLen; indx++) { - i18nObj.registerTranslations(translationFiles[indx]); - } - }); - - describe('getTranslations', function () { - - it('should return the translations for en locale as registered', function () { - const languageTag = ['en']; - const expectedTranslationJson = { - 'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS', - 'test_plugin_1-DEV': 'Run the server with development mode defaults', - 'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server', - 'test_plugin_1-HOME': 'Run along home now!', - 'test_plugin_2-XXXXXX': 'This is XXXXXX string', - 'test_plugin_2-YYYY_PPPP': 'This is YYYY_PPPP string', - 'test_plugin_2-FFFFFFFFFFFF': 'This is FFFFFFFFFFFF string', - 'test_plugin_2-ZZZ': 'This is ZZZ string' - }; - return checkTranslations(expectedTranslationJson, languageTag, i18nObj); - }); - - it('should return the translations for de locale as registered', function () { - const languageTag = ['de']; - const expectedTranslationJson = { - 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS', - 'test_plugin_1-DEV': 'Run the DE server with development mode defaults' - }; - return checkTranslations(expectedTranslationJson, languageTag, i18nObj); - }); - - it('should return the most recently registered translation for a key that has multiple translations', function () { - i18nObj.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_2', 'de.json')); - const languageTag = ['de']; - const expectedTranslationJson = { - 'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS! I am regsitered afterwards!', - 'test_plugin_1-DEV': 'Run the DE server with development mode defaults' - }; - return checkTranslations(expectedTranslationJson, languageTag, i18nObj); - }); - - }); - - }); - - describe('registerTranslations', function () { - - const i18nObj = new I18n(); - - it('should throw error when registering relative path', function () { - return expect(i18nObj.registerTranslations).withArgs('./some/path').to.throwError(); - }); - - it('should throw error when registering empty filename', function () { - return expect(i18nObj.registerTranslations).withArgs('').to.throwError(); - }); - - it('should throw error when registering filename with no extension', function () { - return expect(i18nObj.registerTranslations).withArgs('file1').to.throwError(); - }); - - it('should throw error when registering filename with non JSON extension', function () { - return expect(i18nObj.registerTranslations).withArgs('file1.txt').to.throwError(); - }); - - }); - -}); - -function checkTranslations(expectedTranslations, languageTags, i18nObj) { - return i18nObj.getTranslations(...languageTags) - .then(function (actualTranslations) { - expect(_.isEqual(actualTranslations, expectedTranslations)).to.be(true); - }); -} - -function checkAllTranslations(expectedTranslations, i18nObj) { - return i18nObj.getAllTranslations() - .then(function (actualTranslations) { - expect(_.isEqual(actualTranslations, expectedTranslations)).to.be(true); - }); -} - -function checkTranslationsForDefaultLocale(expectedTranslations, i18nObj) { - return i18nObj.getTranslationsForDefaultLocale() - .then(function (actualTranslations) { - expect(_.isEqual(actualTranslations, expectedTranslations)).to.be(true); - }); -} diff --git a/src/ui/i18n/i18n.js b/src/ui/i18n/i18n.js deleted file mode 100644 index 8e1d4984aff201..00000000000000 --- a/src/ui/i18n/i18n.js +++ /dev/null @@ -1,136 +0,0 @@ -import path from 'path'; -import Promise from 'bluebird'; -import { readFile } from 'fs'; -import _ from 'lodash'; - -const asyncReadFile = Promise.promisify(readFile); - -const TRANSLATION_FILE_EXTENSION = '.json'; - -function getLocaleFromFileName(fullFileName) { - if (_.isEmpty(fullFileName)) throw new Error('Filename empty'); - - const fileExt = path.extname(fullFileName); - if (fileExt.length <= 0 || fileExt !== TRANSLATION_FILE_EXTENSION) { - throw new Error('Translations must be in a JSON file. File being registered is ' + fullFileName); - } - - return path.basename(fullFileName, TRANSLATION_FILE_EXTENSION); -} - -function getBestLocaleMatch(languageTag, registeredLocales) { - if (_.contains(registeredLocales, languageTag)) { - return languageTag; - } - - // Find the first registered locale that begins with one of the language codes from the provided language tag. - // For example, if there is an 'en' language code, it would match an 'en-US' registered locale. - const languageCode = _.first(languageTag.split('-')) || []; - return _.find(registeredLocales, (locale) => _.startsWith(locale, languageCode)); -} - -export class I18n { - - _registeredTranslations = {}; - - constructor(defaultLocale = 'en') { - this._defaultLocale = defaultLocale; - } - - /** - * Return all translations for registered locales - * @return {Promise} translations - A Promise object where keys are - * the locale and values are Objects - * of translation keys and translations - */ - getAllTranslations() { - const localeTranslations = {}; - - const locales = this._getRegisteredTranslationLocales(); - const translations = _.map(locales, (locale) => { - return this._getTranslationsForLocale(locale) - .then(function (translations) { - localeTranslations[locale] = translations; - }); - }); - - return Promise.all(translations) - .then(() => _.assign({}, localeTranslations)); - } - - /** - * Return translations for a suitable locale from a user side locale list - * @param {...string} languageTags - BCP 47 language tags. The tags are listed in priority order as set in the Accept-Language header. - * @returns {Promise} translations - promise for an object where - * keys are translation keys and - * values are translations - * This object will contain all registered translations for the highest priority locale which is registered with the i18n module. - * This object can be empty if no locale in the language tags can be matched against the registered locales. - */ - getTranslations(...languageTags) { - const locale = this._getTranslationLocale(languageTags); - return this._getTranslationsForLocale(locale); - } - - /** - * Return all translations registered for the default locale. - * @returns {Promise} translations - promise for an object where - * keys are translation keys and - * values are translations - */ - getTranslationsForDefaultLocale() { - return this._getTranslationsForLocale(this._defaultLocale); - } - - /** - * The translation file is registered with i18n plugin. The plugin contains a list of registered translation file paths per language. - * @param {String} absolutePluginTranslationFilePath - Absolute path to the translation file to register. - */ - registerTranslations(absolutePluginTranslationFilePath) { - if (!path.isAbsolute(absolutePluginTranslationFilePath)) { - throw new TypeError( - 'Paths to translation files must be absolute. ' + - `Got relative path: "${absolutePluginTranslationFilePath}"` - ); - } - - const locale = getLocaleFromFileName(absolutePluginTranslationFilePath); - - this._registeredTranslations[locale] = - _.uniq(_.get(this._registeredTranslations, locale, []).concat(absolutePluginTranslationFilePath)); - } - - _getRegisteredTranslationLocales() { - return Object.keys(this._registeredTranslations); - } - - _getTranslationLocale(languageTags) { - let locale = ''; - const registeredLocales = this._getRegisteredTranslationLocales(); - _.forEach(languageTags, (tag) => { - locale = locale || getBestLocaleMatch(tag, registeredLocales); - }); - return locale; - } - - _getTranslationsForLocale(locale) { - if (!this._registeredTranslations.hasOwnProperty(locale)) { - return Promise.resolve({}); - } - - const translationFiles = this._registeredTranslations[locale]; - const translations = _.map(translationFiles, (filename) => { - return asyncReadFile(filename, 'utf8') - .then(fileContents => JSON.parse(fileContents)) - .catch(SyntaxError, function () { - throw new Error('Invalid json in ' + filename); - }) - .catch(function () { - throw new Error('Cannot read file ' + filename); - }); - }); - - return Promise.all(translations) - .then(translations => _.assign({}, ...translations)); - } -} diff --git a/src/ui/i18n/index.js b/src/ui/i18n/index.js deleted file mode 100644 index 4738e20b4facf1..00000000000000 --- a/src/ui/i18n/index.js +++ /dev/null @@ -1 +0,0 @@ -export { I18n } from './i18n'; diff --git a/src/ui/ui_i18n/i18n.js b/src/ui/ui_i18n/i18n.js index 8e1d4984aff201..4aa22f1325f5c6 100644 --- a/src/ui/ui_i18n/i18n.js +++ b/src/ui/ui_i18n/i18n.js @@ -30,6 +30,15 @@ function getBestLocaleMatch(languageTag, registeredLocales) { } export class I18n { + static async getAllTranslationsFromPaths(paths) { + const i18n = new I18n(); + + paths.forEach(path => { + i18n.registerTranslations(path); + }); + + return await i18n.getAllTranslations(); + } _registeredTranslations = {}; diff --git a/src/ui/ui_render/views/ui_app.jade b/src/ui/ui_render/views/ui_app.jade index 1595059410bfa0..fdeacf316879c5 100644 --- a/src/ui/ui_render/views/ui_app.jade +++ b/src/ui/ui_render/views/ui_app.jade @@ -108,7 +108,7 @@ block content .kibanaWelcomeLogoCircle .kibanaWelcomeLogo .kibanaWelcomeText - | #{i18n('UI-WELCOME_MESSAGE')} + | #{i18n('UI-WELCARZYCOME_MESSAGE')} script. window.onload = function () { diff --git a/tasks/build/verify_translations.js b/tasks/build/verify_translations.js index ff18b346183634..eee48585f641d2 100644 --- a/tasks/build/verify_translations.js +++ b/tasks/build/verify_translations.js @@ -1,10 +1,10 @@ -import Promise from 'bluebird'; -import _ from 'lodash'; - -import { fromRoot } from '../../src/utils'; -import KbnServer from '../../src/server/kbn_server'; +import { fromRoot, formatListAsProse } from '../../src/utils'; import * as i18nVerify from '../utils/i18n_verify_keys'; +import { findPluginSpecs } from '../../src/plugin_discovery'; +import { collectUiExports } from '../../src/ui'; +import { I18n } from '../../src/ui/ui_i18n/i18n'; + export default function (grunt) { grunt.registerTask('_build:verifyTranslations', [ @@ -12,74 +12,63 @@ export default function (grunt) { '_build:check' ]); - grunt.registerTask('_build:check', function () { + grunt.registerTask('_build:check', async function () { const done = this.async(); - const serverConfig = { - env: 'production', - logging: { - silent: true, - quiet: true, - verbose: false - }, - optimize: { - useBundleCache: false, - enabled: false - }, - server: { - autoListen: false - }, - plugins: { - initialize: true, - scanDirs: [fromRoot('src/core_plugins')] - }, - uiSettings: { - enabled: false - } - }; + try { + const { spec$ } = findPluginSpecs({ + env: 'production', + plugins: { + scanDirs: [fromRoot('src/core_plugins')] + } + }); - const kbnServer = new KbnServer(serverConfig); - kbnServer.ready() - .then(() => verifyTranslations(kbnServer.uiI18n)) - .then(() => kbnServer.close()) - .then(done) - .catch((err) => { - kbnServer.close() - .then(() => done(err)); - }); + const specs = await spec$.toArray().toPromise(); + const uiExports = collectUiExports(specs); + await verifyTranslations(uiExports); + + done(); + } catch (error) { + done(error); + } }); } -function verifyTranslations(uiI18nObj) -{ +async function verifyTranslations(uiExports) { const angularTranslations = require(fromRoot('build/i18n_extract/en.json')); - const translationKeys = Object.keys(angularTranslations); + const keysUsedInViews = Object.keys(angularTranslations); + + // Search files for used translation keys const translationPatterns = [ - { regEx: 'i18n\\(\'(.*)\'\\)', - parsePaths: [fromRoot('/src/ui/views/*.jade')] } + { regexp: 'i18n\\(\'(.*)\'\\)', + parsePaths: [fromRoot('src/ui/ui_render/views/*.jade')] } ]; + for (const { regexp, parsePaths } of translationPatterns) { + const keys = await i18nVerify.getTranslationKeys(regexp, parsePaths); + for (const key of keys) { + keysUsedInViews.push(key); + } + } - const keyPromises = _.map(translationPatterns, (pattern) => { - return i18nVerify.getTranslationKeys(pattern.regEx, pattern.parsePaths) - .then(function (keys) { - const arrayLength = keys.length; - for (let i = 0; i < arrayLength; i++) { - translationKeys.push(keys[i]); - } - }); - }); + // get all of the translations from uiExports + const translations = await I18n.getAllTranslationsFromPaths(uiExports.translationPaths); + const keysWithoutTranslations = Object.entries( + i18nVerify.getNonTranslatedKeys(keysUsedInViews, translations) + ); - return Promise.all(keyPromises) - .then(function () { - return uiI18nObj.getAllTranslations() - .then(function (translations) { - const keysNotTranslatedPerLocale = i18nVerify.getNonTranslatedKeys(translationKeys, translations); - if (!_.isEmpty(keysNotTranslatedPerLocale)) { - const str = JSON.stringify(keysNotTranslatedPerLocale); - const errMsg = 'The following translation keys per locale are not translated: ' + str; - throw new Error(errMsg); - } - }); - }); + if (!keysWithoutTranslations.length) { + return; + } + + throw new Error( + '\n' + + '\n' + + 'The following keys are used in angular/jade views but are not translated:\n' + + keysWithoutTranslations.map(([locale, keys]) => ( + ` - ${locale}: ${formatListAsProse(keys)}` + )).join('\n') + + '\n' + + '\n' + ); } From 4d16a5fed03635be8e414a1b313770871ce07d3c Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 16:43:58 -0700 Subject: [PATCH 10/67] [pluginDiscovery] remove rx utils --- src/plugin_discovery/find_plugin_specs.js | 4 ++-- src/plugin_discovery/plugin_pack/lib/fs.js | 8 +++++--- src/plugin_discovery/plugin_pack/pack_at_path.js | 4 ++-- src/plugin_discovery/utils/combine_latest.js | 5 ----- src/plugin_discovery/utils/concat.js | 5 ----- src/plugin_discovery/utils/create.js | 5 ----- src/plugin_discovery/utils/debug.js | 13 ------------- src/plugin_discovery/utils/defer.js | 5 ----- src/plugin_discovery/utils/empty.js | 3 --- src/plugin_discovery/utils/fcb.js | 14 -------------- src/plugin_discovery/utils/from.js | 5 ----- src/plugin_discovery/utils/from_event.js | 5 ----- src/plugin_discovery/utils/from_promise.js | 5 ----- src/plugin_discovery/utils/index.js | 14 -------------- src/plugin_discovery/utils/merge.js | 5 ----- src/plugin_discovery/utils/of.js | 5 ----- src/plugin_discovery/utils/race.js | 5 ----- src/plugin_discovery/utils/throw.js | 5 ----- src/plugin_discovery/utils/timer.js | 5 ----- 19 files changed, 9 insertions(+), 111 deletions(-) delete mode 100644 src/plugin_discovery/utils/combine_latest.js delete mode 100644 src/plugin_discovery/utils/concat.js delete mode 100644 src/plugin_discovery/utils/create.js delete mode 100644 src/plugin_discovery/utils/debug.js delete mode 100644 src/plugin_discovery/utils/defer.js delete mode 100644 src/plugin_discovery/utils/empty.js delete mode 100644 src/plugin_discovery/utils/fcb.js delete mode 100644 src/plugin_discovery/utils/from.js delete mode 100644 src/plugin_discovery/utils/from_event.js delete mode 100644 src/plugin_discovery/utils/from_promise.js delete mode 100644 src/plugin_discovery/utils/index.js delete mode 100644 src/plugin_discovery/utils/merge.js delete mode 100644 src/plugin_discovery/utils/of.js delete mode 100644 src/plugin_discovery/utils/race.js delete mode 100644 src/plugin_discovery/utils/throw.js delete mode 100644 src/plugin_discovery/utils/timer.js diff --git a/src/plugin_discovery/find_plugin_specs.js b/src/plugin_discovery/find_plugin_specs.js index 7af62ad4fd7ebf..215a2ea553589f 100644 --- a/src/plugin_discovery/find_plugin_specs.js +++ b/src/plugin_discovery/find_plugin_specs.js @@ -1,4 +1,4 @@ -import { $merge } from './utils'; +import { Observable } from 'rxjs'; import { transformDeprecations, Config } from '../server/config'; @@ -42,7 +42,7 @@ function waitForComplete(observable) { */ export function findPluginSpecs(settings, config = defaultConfig(settings)) { // find plugin packs in configured paths/dirs - const find$ = $merge( + const find$ = Observable.merge( ...config.get('plugins.paths').map(createPackAtPath$), ...config.get('plugins.scanDirs').map(createPacksInDirectory$) ) diff --git a/src/plugin_discovery/plugin_pack/lib/fs.js b/src/plugin_discovery/plugin_pack/lib/fs.js index fb21b910a96b6d..9422f87ec08666 100644 --- a/src/plugin_discovery/plugin_pack/lib/fs.js +++ b/src/plugin_discovery/plugin_pack/lib/fs.js @@ -2,8 +2,8 @@ import { stat, readdir } from 'fs'; import { resolve } from 'path'; import { fromNode as fcb } from 'bluebird'; +import { Observable } from 'rxjs'; -import { $fcb, $fromPromise } from '../../utils'; import { createInvalidDirectoryError } from '../../errors'; async function statTest(path, test) { @@ -42,7 +42,8 @@ export async function isDirectory(path) { * @return {Promise>} */ export const createChildDirectory$ = (path) => ( - $fcb(cb => readdir(path, cb)) + Observable + .fromPromise(fcb(cb => readdir(path, cb))) .catch(error => { throw createInvalidDirectoryError(error, path); }) @@ -50,7 +51,8 @@ export const createChildDirectory$ = (path) => ( .filter(name => !name.startsWith('.')) .map(name => resolve(path, name)) .mergeMap(v => ( - $fromPromise(isDirectory(path)) + Observable + .fromPromise(isDirectory(path)) .mergeMap(pass => pass ? [v] : []) )) ); diff --git a/src/plugin_discovery/plugin_pack/pack_at_path.js b/src/plugin_discovery/plugin_pack/pack_at_path.js index 3767577fe24a4c..2ca135d664f600 100644 --- a/src/plugin_discovery/plugin_pack/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/pack_at_path.js @@ -1,4 +1,4 @@ -import { $from } from '../utils'; +import { Observable } from 'rxjs'; import { isAbsolute, resolve } from 'path'; import { createInvalidPackError } from '../errors'; @@ -36,7 +36,7 @@ async function createPackAtPath(path) { } export const createPackAtPath$ = (path) => ( - $from(createPackAtPath(path)) + Observable.from(createPackAtPath(path)) .map(pack => ({ pack })) .catch(error => [{ error }]) ); diff --git a/src/plugin_discovery/utils/combine_latest.js b/src/plugin_discovery/utils/combine_latest.js deleted file mode 100644 index 3ae147004b0255..00000000000000 --- a/src/plugin_discovery/utils/combine_latest.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $combineLatest = (...args) => ( - Rx.Observable.combineLatest(...args) -); diff --git a/src/plugin_discovery/utils/concat.js b/src/plugin_discovery/utils/concat.js deleted file mode 100644 index 9fdadcf32120e1..00000000000000 --- a/src/plugin_discovery/utils/concat.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $concat = (...args) => ( - Rx.Observable.$concat(...args) -); diff --git a/src/plugin_discovery/utils/create.js b/src/plugin_discovery/utils/create.js deleted file mode 100644 index af34ae6f46bc32..00000000000000 --- a/src/plugin_discovery/utils/create.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $create = (block) => ( - Rx.Observable.create(block) -); diff --git a/src/plugin_discovery/utils/debug.js b/src/plugin_discovery/utils/debug.js deleted file mode 100644 index a81d9d13badbea..00000000000000 --- a/src/plugin_discovery/utils/debug.js +++ /dev/null @@ -1,13 +0,0 @@ -export function debug(name) { - return { - next(v) { - console.log('N: %s %s', name, v); - }, - error(error) { - console.log('E: %s', name, error); - }, - complete() { - console.log('C: %s', name); - } - }; -} diff --git a/src/plugin_discovery/utils/defer.js b/src/plugin_discovery/utils/defer.js deleted file mode 100644 index 2ee970ccd73bae..00000000000000 --- a/src/plugin_discovery/utils/defer.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $defer = (...args) => ( - Rx.Observable.defer(...args) -); diff --git a/src/plugin_discovery/utils/empty.js b/src/plugin_discovery/utils/empty.js deleted file mode 100644 index e3bf3916cf348f..00000000000000 --- a/src/plugin_discovery/utils/empty.js +++ /dev/null @@ -1,3 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const empty$ = Rx.Observable.empty(); diff --git a/src/plugin_discovery/utils/fcb.js b/src/plugin_discovery/utils/fcb.js deleted file mode 100644 index eaf43d1526c762..00000000000000 --- a/src/plugin_discovery/utils/fcb.js +++ /dev/null @@ -1,14 +0,0 @@ -import { $create } from './create'; - -export const $fcb = (block) => ( - $create(observer => { - block((error, value) => { - if (error) { - observer.error(error); - } else { - observer.next(value); - observer.complete(observer); - } - }); - }) -); diff --git a/src/plugin_discovery/utils/from.js b/src/plugin_discovery/utils/from.js deleted file mode 100644 index 51490bf4d40982..00000000000000 --- a/src/plugin_discovery/utils/from.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $from = (input) => ( - Rx.Observable.from(input) -); diff --git a/src/plugin_discovery/utils/from_event.js b/src/plugin_discovery/utils/from_event.js deleted file mode 100644 index f7d37b618eec8c..00000000000000 --- a/src/plugin_discovery/utils/from_event.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $fromEvent = (...args) => ( - Rx.Observable.fromEvent(...args) -); diff --git a/src/plugin_discovery/utils/from_promise.js b/src/plugin_discovery/utils/from_promise.js deleted file mode 100644 index c29c70fe292694..00000000000000 --- a/src/plugin_discovery/utils/from_promise.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $fromPromise = (...args) => ( - Rx.Observable.fromPromise(...args) -); diff --git a/src/plugin_discovery/utils/index.js b/src/plugin_discovery/utils/index.js deleted file mode 100644 index 64397b8dd1ab33..00000000000000 --- a/src/plugin_discovery/utils/index.js +++ /dev/null @@ -1,14 +0,0 @@ -export { $combineLatest } from './combine_latest'; -export { $concat } from './concat'; -export { $create } from './create'; -export { $defer } from './defer'; -export { empty$ } from './empty'; -export { $fcb } from './fcb'; -export { $from } from './from'; -export { $fromEvent } from './from_event'; -export { $fromPromise } from './from_promise'; -export { $merge } from './merge'; -export { $of } from './of'; -export { $race } from './race'; -export { $timer } from './timer'; -export { $throw } from './throw'; diff --git a/src/plugin_discovery/utils/merge.js b/src/plugin_discovery/utils/merge.js deleted file mode 100644 index 09d1cce19e7b86..00000000000000 --- a/src/plugin_discovery/utils/merge.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $merge = (...args) => ( - Rx.Observable.merge(...args) -); diff --git a/src/plugin_discovery/utils/of.js b/src/plugin_discovery/utils/of.js deleted file mode 100644 index ad9c4cc2dec279..00000000000000 --- a/src/plugin_discovery/utils/of.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $of = (...args) => ( - Rx.Observable.of(...args) -); diff --git a/src/plugin_discovery/utils/race.js b/src/plugin_discovery/utils/race.js deleted file mode 100644 index 18dcf4bfd193a5..00000000000000 --- a/src/plugin_discovery/utils/race.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $race = (...args) => ( - Rx.Observable.race(...args) -); diff --git a/src/plugin_discovery/utils/throw.js b/src/plugin_discovery/utils/throw.js deleted file mode 100644 index 8935307a66d749..00000000000000 --- a/src/plugin_discovery/utils/throw.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $throw = (...args) => ( - Rx.Observable.throw(...args) -); diff --git a/src/plugin_discovery/utils/timer.js b/src/plugin_discovery/utils/timer.js deleted file mode 100644 index 8bf2069adb7766..00000000000000 --- a/src/plugin_discovery/utils/timer.js +++ /dev/null @@ -1,5 +0,0 @@ -import Rx from 'rxjs/Rx'; - -export const $timer = (...args) => ( - Rx.Observable.timer(...args) -); From ca727dc921f99ddd6c37ae78697022054fa2e650 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 2 Nov 2017 16:46:00 -0700 Subject: [PATCH 11/67] fix i18n transaltion key name --- src/ui/ui_render/views/ui_app.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/ui_render/views/ui_app.jade b/src/ui/ui_render/views/ui_app.jade index fdeacf316879c5..1595059410bfa0 100644 --- a/src/ui/ui_render/views/ui_app.jade +++ b/src/ui/ui_render/views/ui_app.jade @@ -108,7 +108,7 @@ block content .kibanaWelcomeLogoCircle .kibanaWelcomeLogo .kibanaWelcomeText - | #{i18n('UI-WELCARZYCOME_MESSAGE')} + | #{i18n('UI-WELCOME_MESSAGE')} script. window.onload = function () { From 710254f030f3175bc876f3da50170f62f6092c90 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 8 Nov 2017 13:50:52 -0700 Subject: [PATCH 12/67] [pluginDiscovery] do kibana version checks as a part of discovery --- src/plugin_discovery/errors.js | 30 +++++++++++++++++ src/plugin_discovery/find_plugin_specs.js | 21 +++++++++--- .../plugin_pack/pack_at_path.js | 2 +- .../plugin_spec}/is_version_compatible.js | 2 +- .../plugin_spec/plugin_spec.js | 21 ++++++++---- src/server/kbn_server.js | 3 -- src/server/plugins/check_versions_mixin.js | 32 ------------------- src/server/plugins/index.js | 1 - src/server/plugins/lib/index.js | 1 - src/server/plugins/lib/plugin.js | 1 - src/server/plugins/scan_mixin.js | 13 ++++++++ 11 files changed, 76 insertions(+), 51 deletions(-) rename src/{server/plugins/lib => plugin_discovery/plugin_spec}/is_version_compatible.js (92%) delete mode 100644 src/server/plugins/check_versions_mixin.js diff --git a/src/plugin_discovery/errors.js b/src/plugin_discovery/errors.js index 7553b7ef86b07d..7315e1aea4cb3b 100644 --- a/src/plugin_discovery/errors.js +++ b/src/plugin_discovery/errors.js @@ -31,3 +31,33 @@ export function createInvalidPackError(path, reason) { export function isInvalidPackError(error) { return error && error[$code] === ERROR_INVALID_PACK; } + +/** + * Thrown when trying to load a PluginSpec that is invalid for some reason + * @type {String} + */ +const ERROR_INVALID_PLUGIN = 'ERROR_INVALID_PLUGIN'; +export function createInvalidPluginError(spec, reason) { + const error = new Error(`Plugin from ${spec.getId()} from ${spec.getPack().getPath()} is invalid because ${reason}`); + error[$code] = ERROR_INVALID_PLUGIN; + error.spec = spec; + return error; +} +export function isInvalidPluginError(error) { + return error && error[$code] === ERROR_INVALID_PLUGIN; +} + +/** + * Thrown when trying to load a PluginSpec whose version is incompatible + * @type {String} + */ +const ERROR_INCOMPATIBLE_PLUGIN_VERSION = 'ERROR_INCOMPATIBLE_PLUGIN_VERSION'; +export function createIncompatiblePluginVersionError(spec) { + const error = new Error(`Plugin ${spec.getId()} is only compatible with Kibana version ${spec.getExpectedKibanaVersion()}`); + error[$code] = ERROR_INCOMPATIBLE_PLUGIN_VERSION; + error.spec = spec; + return error; +} +export function isIncompatiblePluginVersionError(error) { + return error && error[$code] === ERROR_INCOMPATIBLE_PLUGIN_VERSION; +} diff --git a/src/plugin_discovery/find_plugin_specs.js b/src/plugin_discovery/find_plugin_specs.js index 215a2ea553589f..e5921ea71d2fb0 100644 --- a/src/plugin_discovery/find_plugin_specs.js +++ b/src/plugin_discovery/find_plugin_specs.js @@ -69,12 +69,15 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { }) // extend the config with all plugins before determining enabled status .let(waitForComplete) - .map(result => { - const enabled = result.spec.isEnabled(config); + .map(({ spec, deprecations }) => { + const rightVersion = spec.isVersionCompatible(config.get('pkg.version')); + const enabled = rightVersion && spec.isEnabled(config); return { - ...result, - enabledSpecs: enabled ? [result.spec] : [], - disabledSpecs: enabled ? [] : [result.spec] + spec, + deprecations, + enabledSpecs: enabled ? [spec] : [], + disabledSpecs: enabled ? [] : [spec], + invalidVersionSpecs: rightVersion ? [] : [spec], }; }) // determin which plugins are disabled before actually removing things from the config @@ -119,5 +122,13 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { // all enabled PluginSpec objects spec$: extendConfig$ .mergeMap(result => result.enabledSpecs), + + // all disabled PluginSpec objects + disabledSpecs$: extendConfig$ + .mergeMap(result => result.disabledSpecs), + + // all PluginSpec objects that were disabled because their version was incompatible + invalidVersionSpecs$: extendConfig$ + .mergeMap(result => result.invalidVersionSpecs), }; } diff --git a/src/plugin_discovery/plugin_pack/pack_at_path.js b/src/plugin_discovery/plugin_pack/pack_at_path.js index 2ca135d664f600..646d4abe447b99 100644 --- a/src/plugin_discovery/plugin_pack/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/pack_at_path.js @@ -36,7 +36,7 @@ async function createPackAtPath(path) { } export const createPackAtPath$ = (path) => ( - Observable.from(createPackAtPath(path)) + Observable.fromPromise(createPackAtPath(path)) .map(pack => ({ pack })) .catch(error => [{ error }]) ); diff --git a/src/server/plugins/lib/is_version_compatible.js b/src/plugin_discovery/plugin_spec/is_version_compatible.js similarity index 92% rename from src/server/plugins/lib/is_version_compatible.js rename to src/plugin_discovery/plugin_spec/is_version_compatible.js index d120b1a21951f9..139f75566735a9 100644 --- a/src/server/plugins/lib/is_version_compatible.js +++ b/src/plugin_discovery/plugin_spec/is_version_compatible.js @@ -1,7 +1,7 @@ import { cleanVersion, versionSatisfies -} from '../../../utils/version'; +} from '../../utils/version'; export function isVersionCompatible(version, compatibleWith) { // the special "kibana" version can be used to always be compatible, diff --git a/src/plugin_discovery/plugin_spec/plugin_spec.js b/src/plugin_discovery/plugin_spec/plugin_spec.js index 1f971c8f887469..7e7c30cb3fe436 100644 --- a/src/plugin_discovery/plugin_spec/plugin_spec.js +++ b/src/plugin_discovery/plugin_spec/plugin_spec.js @@ -3,6 +3,9 @@ import { resolve, basename, isAbsolute as isAbsolutePath } from 'path'; import toPath from 'lodash/internal/toPath'; import { get } from 'lodash'; +import { createInvalidPluginError } from '../errors'; +import { isVersionCompatible } from './is_version_compatible'; + export class PluginSpec { /** * @param {PluginPack} pack - The plugin pack that produced this spec @@ -36,6 +39,7 @@ export class PluginSpec { const { id, require, + version, kibanaVersion, uiExports, publicDir, @@ -49,6 +53,7 @@ export class PluginSpec { this._id = id; this._pack = pack; + this._version = version; this._kibanaVersion = kibanaVersion; this._require = require; @@ -64,23 +69,23 @@ export class PluginSpec { this._init = init; if (!this.getId()) { - throw new Error('Unable to determine plugin id'); + throw createInvalidPluginError(this, 'Unable to determine plugin id'); } if (!this.getVersion()) { - throw new TypeError('Unable to determin plugin version'); + throw createInvalidPluginError(this, 'Unable to determine plugin version'); } if (this.getRequiredPluginIds() !== undefined && !Array.isArray(this.getRequiredPluginIds())) { - throw new TypeError('"plugin.require" must be an array of plugin ids'); + throw createInvalidPluginError(this, '"plugin.require" must be an array of plugin ids'); } if (this._publicDir) { if (!isAbsolutePath(this._publicDir)) { - throw new Error('plugin.publicDir must be an absolute path'); + throw createInvalidPluginError(this, 'plugin.publicDir must be an absolute path'); } if (basename(this._publicDir) !== 'public') { - throw new Error(`publicDir for plugin ${this.getId()} must end with a "public" directory.`); + throw createInvalidPluginError(this, `publicDir for plugin ${this.getId()} must end with a "public" directory.`); } } } @@ -102,7 +107,7 @@ export class PluginSpec { } getVersion() { - return this.getPkg().version; + return this._version || this.getPkg().version; } isEnabled(config) { @@ -121,6 +126,10 @@ export class PluginSpec { return this._kibanaVersion || get(this.getPack().getPkg(), 'kibana.version') || this.getVersion(); } + isVersionCompatible(actualKibanaVersion) { + return isVersionCompatible(this.getExpectedKibanaVersion(), actualKibanaVersion); + } + getRequiredPluginIds() { return this._require; } diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 98aa55a55a88e4..504ea2f4091ae9 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -52,9 +52,6 @@ export default class KbnServer { // find plugins and set this.plugins Plugins.scanMixin, - // disable the plugins that are incompatible with the current version of Kibana - Plugins.checkVersionsMixin, - // tell the config we are done loading plugins configCompleteMixin, diff --git a/src/server/plugins/check_versions_mixin.js b/src/server/plugins/check_versions_mixin.js deleted file mode 100644 index 2f5e1192ce4ecb..00000000000000 --- a/src/server/plugins/check_versions_mixin.js +++ /dev/null @@ -1,32 +0,0 @@ -import { isVersionCompatible } from './lib'; - -/** - * Check that plugin versions match Kibana version, otherwise - * disable them - * - * @param {KbnServer} kbnServer - * @param {Hapi.Server} server - * @return {Promise} - */ -export function checkVersionsMixin(kbnServer, server) { - // because a plugin pack can contain more than one actual plugin, (for example x-pack) - // we make sure that the warning messages are unique - const warningMessages = new Set(); - const plugins = kbnServer.plugins; - const kibanaVersion = kbnServer.version; - - for (const plugin of plugins) { - const name = plugin.id; - const pluginVersion = plugin.kibanaVersion; - - if (!isVersionCompatible(pluginVersion, kibanaVersion)) { - const message = `Plugin "${name}" was disabled because it expected Kibana version "${pluginVersion}", and found "${kibanaVersion}".`; - warningMessages.add(message); - plugins.disable(plugin); - } - } - - for (const message of warningMessages) { - server.log(['warning'], message); - } -} diff --git a/src/server/plugins/index.js b/src/server/plugins/index.js index fc6820373d1d5d..eb8da1af94752a 100644 --- a/src/server/plugins/index.js +++ b/src/server/plugins/index.js @@ -1,4 +1,3 @@ export { scanMixin } from './scan_mixin'; -export { checkVersionsMixin } from './check_versions_mixin'; export { initializeMixin } from './initialize_mixin'; export { waitForInitSetupMixin, waitForInitResolveMixin } from './wait_for_plugins_init'; diff --git a/src/server/plugins/lib/index.js b/src/server/plugins/lib/index.js index ec0d4d0bc67d7a..2c65f1d1df9e5e 100644 --- a/src/server/plugins/lib/index.js +++ b/src/server/plugins/lib/index.js @@ -1,3 +1,2 @@ export { callPluginHook } from './call_plugin_hook'; -export { isVersionCompatible } from './is_version_compatible'; export { Plugin } from './plugin'; diff --git a/src/server/plugins/lib/plugin.js b/src/server/plugins/lib/plugin.js index df435b9550aa95..d50096b828c56c 100644 --- a/src/server/plugins/lib/plugin.js +++ b/src/server/plugins/lib/plugin.js @@ -22,7 +22,6 @@ export class Plugin { this.id = spec.getId(); this.version = spec.getVersion(); this.requiredIds = spec.getRequiredPluginIds() || []; - this.kibanaVersion = spec.getExpectedKibanaVersion(); this.externalPreInit = spec.getPreInitHandler(); this.externalInit = spec.getInitHandler(); this.enabled = spec.isEnabled(kbnServer.config); diff --git a/src/server/plugins/scan_mixin.js b/src/server/plugins/scan_mixin.js index bda5db6f12b12b..67b09525682d71 100644 --- a/src/server/plugins/scan_mixin.js +++ b/src/server/plugins/scan_mixin.js @@ -9,6 +9,7 @@ export async function scanMixin(kbnServer, server, config) { invalidDirectoryError$, invalidPackError$, deprecation$, + invalidVersionSpecs$, spec$, } = findPluginSpecs(kbnServer.settings, config); @@ -35,6 +36,18 @@ export async function scanMixin(kbnServer, server, config) { }); }), + invalidVersionSpecs$ + .map(spec => { + const name = spec.getId(); + const pluginVersion = spec.getExpectedKibanaVersion(); + const kibanaVersion = config.get('pkg.version'); + return `Plugin "${name}" was disabled because it expected Kibana version "${pluginVersion}", and found "${kibanaVersion}".`; + }) + .distinct() + .do(message => { + server.log(['plugin', 'warning'], message); + }), + deprecation$.do(({ spec, message }) => { server.log(['warning', spec.getConfigPrefix(), 'config', 'deprecation'], message); }) From 75fb29ec3511140326e27cdd13e5cabb4d8fc52a Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 8 Nov 2017 13:54:41 -0700 Subject: [PATCH 13/67] [pluginDiscovery/createPacksInDirectory$] clarify error handling --- src/plugin_discovery/plugin_pack/packs_in_directory.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugin_discovery/plugin_pack/packs_in_directory.js b/src/plugin_discovery/plugin_pack/packs_in_directory.js index 1caa2c5ad3d44a..ad24743367f0b6 100644 --- a/src/plugin_discovery/plugin_pack/packs_in_directory.js +++ b/src/plugin_discovery/plugin_pack/packs_in_directory.js @@ -20,8 +20,10 @@ export const createPacksInDirectory$ = (path) => ( createChildDirectory$(path) .mergeMap(createPackAtPath$) .catch(error => { + // this error is produced by createChildDirectory$() when the path + // is invalid, we return them as an error result similar to how + // createPackAtPath$ works when it finds invalid packs in a directory if (isInvalidDirectoryError(path)) { - // these are expected errors that we should return as "results" return [{ error }]; } From 100a306fc45575c6d0f2b5bf631a1a9804a337db Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 14 Nov 2017 21:37:09 -0600 Subject: [PATCH 14/67] [eslint] fix lint errors --- src/plugin_discovery/find_plugin_specs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin_discovery/find_plugin_specs.js b/src/plugin_discovery/find_plugin_specs.js index e5921ea71d2fb0..491b784997e9dd 100644 --- a/src/plugin_discovery/find_plugin_specs.js +++ b/src/plugin_discovery/find_plugin_specs.js @@ -46,7 +46,7 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { ...config.get('plugins.paths').map(createPackAtPath$), ...config.get('plugins.scanDirs').map(createPacksInDirectory$) ) - .share(); + .share(); const extendConfig$ = find$ // get the specs for each found plugin pack From eb9876cf1435d0d9f95ec381301d57b05ca2d128 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:05:59 -0600 Subject: [PATCH 15/67] [uiApp/modules] ensure load order matches master --- src/ui/ui_apps/ui_app.js | 13 +++++++------ src/ui/ui_exports/ui_export_types/ui_apps.js | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ui/ui_apps/ui_app.js b/src/ui/ui_apps/ui_app.js index eaad8671e4a0e1..01759b3c1aa18d 100644 --- a/src/ui/ui_apps/ui_app.js +++ b/src/ui/ui_apps/ui_app.js @@ -57,12 +57,13 @@ export class UiApp { }; const { appExtensions = [] } = kbnServer.uiExports; - this._modules = [].concat( - this._main, - ...uses.map(type => appExtensions[type] || []), - appExtensions.chromeNavControls || [], - appExtensions.hacks || [] - ); + this._modules = [] + .concat(this._main, ...uses.map(type => appExtensions[type] || [])) + .reduce((modules, item) => ( + modules.includes(item) + ? modules + : modules.concat(item) + ), []); if (!this.isHidden()) { // unless an app is hidden it gets a navlink, but we only respond to `getNavLink()` diff --git a/src/ui/ui_exports/ui_export_types/ui_apps.js b/src/ui/ui_exports/ui_export_types/ui_apps.js index 07203fe97cf4c7..c11977a03bd161 100644 --- a/src/ui/ui_exports/ui_export_types/ui_apps.js +++ b/src/ui/ui_exports/ui_export_types/ui_apps.js @@ -37,8 +37,8 @@ function applySpecDefaults(spec, type, pluginSpec) { url, uses: uniq([ ...uses, + 'chromeNavControls', 'hacks', - 'chromeNavControls' ]), }; } From 604344a87f5647e29e3b5c3ad644ba5d2a13de56 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:06:48 -0600 Subject: [PATCH 16/67] [uiBundle] use known uiExport type for providers --- src/ui/ui_bundles/ui_bundles_mixin.js | 8 +++----- src/ui/ui_exports/ui_export_types/index.js | 1 + .../ui_exports/ui_export_types/webpack_customizations.js | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ui/ui_bundles/ui_bundles_mixin.js b/src/ui/ui_bundles/ui_bundles_mixin.js index 07c739e04e83f5..9c39883c19dfe6 100644 --- a/src/ui/ui_bundles/ui_bundles_mixin.js +++ b/src/ui/ui_bundles/ui_bundles_mixin.js @@ -3,10 +3,8 @@ import { UiBundlesController } from './ui_bundles_controller'; export async function uiBundlesMixin(kbnServer) { kbnServer.uiBundles = new UiBundlesController(kbnServer); - const { unknown = [] } = kbnServer.uiExports; - for (const { type, spec } of unknown) { - if (type === '__bundleProvider__') { - await spec(kbnServer); - } + const { uiBundleProviders = [] } = kbnServer.uiExports; + for (const spec of uiBundleProviders) { + await spec(kbnServer); } } diff --git a/src/ui/ui_exports/ui_export_types/index.js b/src/ui/ui_exports/ui_export_types/index.js index 41868d6f64890c..099caf51fa3447 100644 --- a/src/ui/ui_exports/ui_export_types/index.js +++ b/src/ui/ui_exports/ui_export_types/index.js @@ -52,4 +52,5 @@ export { export { noParse, __globalImportAliases__, + __bundleProvider__, } from './webpack_customizations'; diff --git a/src/ui/ui_exports/ui_export_types/webpack_customizations.js b/src/ui/ui_exports/ui_export_types/webpack_customizations.js index 899aa1ee9f1fa6..d2096ecc48224c 100644 --- a/src/ui/ui_exports/ui_export_types/webpack_customizations.js +++ b/src/ui/ui_exports/ui_export_types/webpack_customizations.js @@ -6,6 +6,7 @@ import { concat, merge } from './reduce'; import { alias, wrap, uniqueKeys, mapSpec } from './modify_reduce'; export const __globalImportAliases__ = wrap(alias('webpackAliases'), uniqueKeys('__globalImportAliases__'), merge); +export const __bundleProvider__ = wrap(alias('uiBundleProviders'), concat); export const noParse = wrap( alias('webpackNoParseRules'), mapSpec(rule => { From 6e7bef9c4c5ba877dbfcbc253a1ff471747edae1 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:07:57 -0600 Subject: [PATCH 17/67] [uiExports] use the `home` export type --- src/ui/ui_exports/ui_export_types/index.js | 3 ++- .../ui_export_types/{ui_extensions.js => ui_app_extensions.js} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename src/ui/ui_exports/ui_export_types/{ui_extensions.js => ui_app_extensions.js} (100%) diff --git a/src/ui/ui_exports/ui_export_types/index.js b/src/ui/ui_exports/ui_export_types/index.js index 099caf51fa3447..53f080720f3d10 100644 --- a/src/ui/ui_exports/ui_export_types/index.js +++ b/src/ui/ui_exports/ui_export_types/index.js @@ -28,9 +28,10 @@ export { devTools, docViews, hacks, + home, visTypeEnhancers, aliases, -} from './ui_extensions'; +} from './ui_app_extensions'; export { translations, diff --git a/src/ui/ui_exports/ui_export_types/ui_extensions.js b/src/ui/ui_exports/ui_export_types/ui_app_extensions.js similarity index 100% rename from src/ui/ui_exports/ui_export_types/ui_extensions.js rename to src/ui/ui_exports/ui_export_types/ui_app_extensions.js From 00c90205bd2f21336f69eeaa1e34f3a093c97731 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:08:37 -0600 Subject: [PATCH 18/67] [uiExports] validate that all uiExport types are known --- src/ui/ui_exports/index.js | 1 + src/ui/ui_exports/ui_exports_mixin.js | 19 +++++++++++++++++++ src/ui/ui_mixin.js | 7 ++----- 3 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 src/ui/ui_exports/ui_exports_mixin.js diff --git a/src/ui/ui_exports/index.js b/src/ui/ui_exports/index.js index 1326e6767489d2..a9a1e989fbe729 100644 --- a/src/ui/ui_exports/index.js +++ b/src/ui/ui_exports/index.js @@ -1 +1,2 @@ export { collectUiExports } from './collect_ui_exports'; +export { uiExportsMixin } from './ui_exports_mixin'; diff --git a/src/ui/ui_exports/ui_exports_mixin.js b/src/ui/ui_exports/ui_exports_mixin.js new file mode 100644 index 00000000000000..d36c8f96016f26 --- /dev/null +++ b/src/ui/ui_exports/ui_exports_mixin.js @@ -0,0 +1,19 @@ +import { collectUiExports } from './collect_ui_exports'; + +export function uiExportsMixin(kbnServer) { + kbnServer.uiExports = collectUiExports( + kbnServer.pluginSpecs + ); + + // check for unknown uiExport types + const { unknown = [] } = kbnServer.uiExports; + if (!unknown.length) { + return; + } + + throw new Error(`Unknown uiExport types: ${ + unknown + .map(({ pluginSpec, type }) => `${type} from ${pluginSpec.getId()}`) + .join(', ') + }`); +} diff --git a/src/ui/ui_mixin.js b/src/ui/ui_mixin.js index 753afe93f1e3f3..0c94fc457c5b66 100644 --- a/src/ui/ui_mixin.js +++ b/src/ui/ui_mixin.js @@ -1,4 +1,4 @@ -import { collectUiExports } from './ui_exports'; +import { uiExportsMixin } from './ui_exports'; import { fieldFormatsMixin } from './field_formats'; import { uiAppsMixin } from './ui_apps'; import { uiI18nMixin } from './ui_i18n'; @@ -8,10 +8,7 @@ import { uiRenderMixin } from './ui_render'; import { uiSettingsMixin } from './ui_settings'; export async function uiMixin(kbnServer) { - kbnServer.uiExports = collectUiExports( - kbnServer.pluginSpecs - ); - + await kbnServer.mixin(uiExportsMixin); await kbnServer.mixin(uiAppsMixin); await kbnServer.mixin(uiBundlesMixin); await kbnServer.mixin(uiSettingsMixin); From 78951ec1b7f6994841b6e25c6a028f209b2b689e Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:13:00 -0600 Subject: [PATCH 19/67] [timelion] remove archaic/broken bwc check --- src/core_plugins/timelion/index.js | 13 +------------ src/core_plugins/timelion/public/app.js | 1 + .../timelion/public/app_with_autoload.js | 2 -- 3 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 src/core_plugins/timelion/public/app_with_autoload.js diff --git a/src/core_plugins/timelion/index.js b/src/core_plugins/timelion/index.js index 109abcb59dcfc0..98f6121e556112 100644 --- a/src/core_plugins/timelion/index.js +++ b/src/core_plugins/timelion/index.js @@ -1,15 +1,4 @@ export default function (kibana) { - let mainFile = 'plugins/timelion/app'; - - const ownDescriptor = Object.getOwnPropertyDescriptor(kibana, 'autoload'); - const protoDescriptor = Object.getOwnPropertyDescriptor(kibana.constructor.prototype, 'autoload'); - const descriptor = ownDescriptor || protoDescriptor || {}; - if (descriptor.get) { - // the autoload list has been replaced with a getter that complains about - // improper access, bypass that getter by seeing if it is defined - mainFile = 'plugins/timelion/app_with_autoload'; - } - return new kibana.Plugin({ require: ['kibana', 'elasticsearch'], uiExports: { @@ -18,7 +7,7 @@ export default function (kibana) { order: -1000, description: 'Time series expressions for everything', icon: 'plugins/timelion/icon.svg', - main: mainFile, + main: 'plugins/timelion/app', injectVars: function (server) { const config = server.config(); return { diff --git a/src/core_plugins/timelion/public/app.js b/src/core_plugins/timelion/public/app.js index 27907fbd745758..c34a001ee77fca 100644 --- a/src/core_plugins/timelion/public/app.js +++ b/src/core_plugins/timelion/public/app.js @@ -6,6 +6,7 @@ import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_regis import { notify } from 'ui/notify'; import { timezoneProvider } from 'ui/vis/lib/timezone'; +require('ui/autoload/all'); require('plugins/timelion/directives/cells/cells'); require('plugins/timelion/directives/fixed_element'); require('plugins/timelion/directives/fullscreen/fullscreen'); diff --git a/src/core_plugins/timelion/public/app_with_autoload.js b/src/core_plugins/timelion/public/app_with_autoload.js deleted file mode 100644 index 66d538a2601748..00000000000000 --- a/src/core_plugins/timelion/public/app_with_autoload.js +++ /dev/null @@ -1,2 +0,0 @@ -require('ui/autoload/all'); -require('./app'); From b37bc7c3db4c771b81e55d1256a8c37834fae566 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:29:53 -0600 Subject: [PATCH 20/67] revert some stragler changes --- src/ui/public/filter_bar/filter_bar.js | 3 +++ src/ui/public/saved_objects/saved_objects_client.js | 8 +------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ui/public/filter_bar/filter_bar.js b/src/ui/public/filter_bar/filter_bar.js index d518b65ba32c2a..29efc116c53931 100644 --- a/src/ui/public/filter_bar/filter_bar.js +++ b/src/ui/public/filter_bar/filter_bar.js @@ -13,6 +13,9 @@ import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; import { compareFilters } from './lib/compare_filters'; import { uiModules } from 'ui/modules'; +export { disableFilter, enableFilter, toggleFilterDisabled } from './lib/disable_filter'; + + const module = uiModules.get('kibana'); module.directive('filterBar', function (Private, Promise, getAppState) { diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js index a85c238ccee2fa..c0f34c3c100cb3 100644 --- a/src/ui/public/saved_objects/saved_objects_client.js +++ b/src/ui/public/saved_objects/saved_objects_client.js @@ -180,13 +180,7 @@ export class SavedObjectsClient { return resolveUrl(this._apiBaseUrl, formatUrl({ pathname: join(...path), - query: _.pick(query, value => { - if (Array.isArray(value)) { - return !!value.length; - } - - return value != null; - }) + query: _.pick(query, value => value != null) })); } From eeb65aea75709c1ba86bb52e3237cc74a088ee75 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:32:16 -0600 Subject: [PATCH 21/67] [pluginSpecs] reformat comments --- .../plugin_spec/plugin_spec.js | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/plugin_discovery/plugin_spec/plugin_spec.js b/src/plugin_discovery/plugin_spec/plugin_spec.js index 7e7c30cb3fe436..0e3681d5976f43 100644 --- a/src/plugin_discovery/plugin_spec/plugin_spec.js +++ b/src/plugin_discovery/plugin_spec/plugin_spec.js @@ -8,32 +8,27 @@ import { isVersionCompatible } from './is_version_compatible'; export class PluginSpec { /** - * @param {PluginPack} pack - The plugin pack that produced this spec - * @param {Object} opts - the options for this plugin - * @param {String} [opts.id=pkg.name] - the id for this plugin. - * @param {Object} [opts.uiExports] - a mapping of UiExport types - * to UI modules or metadata about - * the UI module - * @param {Array} [opts.require] - the other plugins that this plugin - * requires. These plugins must exist and - * be enabled for this plugin to function. - * The require'd plugins will also be - * initialized first, in order to make sure - * that dependencies provided by these plugins - * are available - * @param {String} [opts.version=pkg.version] - the version of this plugin - * @param {Function} [opts.init] - A function that will be called to initialize - * this plugin at the appropriate time. - * @param {Function} [opts.configPrefix=this.id] - The prefix to use for configuration - * values in the main configuration service - * @param {Function} [opts.config] - A function that produces a configuration - * schema using Joi, which is passed as its - * first argument. - * @param {String|False} [opts.publicDir=path + '/public'] - * - the public directory for this plugin. The final directory must - * have the name "public", though it can be located somewhere besides - * the root of the plugin. Set this to false to disable exposure of a - * public directory + * @param {PluginPack} pack The plugin pack that produced this spec + * @param {Object} opts the options for this plugin + * @param {String} [opts.id=pkg.name] the id for this plugin. + * @param {Object} [opts.uiExports] a mapping of UiExport types to + * UI modules or metadata about the UI module + * @param {Array} [opts.require] the other plugins that this plugin + * requires. These plugins must exist and be enabled for this plugin + * to function. The require'd plugins will also be initialized first, + * in order to make sure that dependencies provided by these plugins + * are available + * @param {String} [opts.version=pkg.version] the version of this plugin + * @param {Function} [opts.init] A function that will be called to initialize + * this plugin at the appropriate time. + * @param {Function} [opts.configPrefix=this.id] The prefix to use for + * configuration values in the main configuration service + * @param {Function} [opts.config] A function that produces a configuration + * schema using Joi, which is passed as its first argument. + * @param {String|False} [opts.publicDir=path + '/public'] the public + * directory for this plugin. The final directory must have the name "public", + * though it can be located somewhere besides the root of the plugin. Set + * this to false to disable exposure of a public directory */ constructor(pack, options) { const { From 00cbaedba89557d64cbb6f7728919496dcb1a5eb Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:35:11 -0600 Subject: [PATCH 22/67] =?UTF-8?q?[uiBundle]=20rebel=20and=20use=20more=20f?= =?UTF-8?q?cb=20=F0=9F=98=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/ui_bundles/ui_bundle.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/ui/ui_bundles/ui_bundle.js b/src/ui/ui_bundles/ui_bundle.js index a73b4137562b4d..aa9f2892a9069a 100644 --- a/src/ui/ui_bundles/ui_bundle.js +++ b/src/ui/ui_bundles/ui_bundle.js @@ -1,9 +1,5 @@ -import { promisify } from 'bluebird'; - -const read = promisify(require('fs').readFile); -const write = promisify(require('fs').writeFile); -const unlink = promisify(require('fs').unlink); -const stat = promisify(require('fs').stat); +import { fromNode as fcb } from 'bluebird'; +import { readFile, writeFile, unlink, stat } from 'fs'; export class UiBundle { constructor(options) { @@ -48,7 +44,7 @@ export class UiBundle { async readEntryFile() { try { - const content = await read(this.getEntryPath()); + const content = await fcb(cb => readFile(this.getEntryPath(), cb)); return content.toString('utf8'); } catch (e) { @@ -57,17 +53,22 @@ export class UiBundle { } async writeEntryFile() { - return await write(this.getEntryPath(), this.renderContent(), { encoding: 'utf8' }); + return await fcb(cb => ( + writeFile(this.getEntryPath(), this.renderContent(), 'utf8', cb) + )); } async clearBundleFile() { - try { await unlink(this.getOutputPath()); } - catch (e) { return null; } + try { + await fcb(cb => unlink(this.getOutputPath(), cb)); + } catch (e) { + return null; + } } async isCacheValid() { try { - await stat(this.getOutputPath()); + await fcb(cb => stat(this.getOutputPath(), cb)); return true; } catch (e) { From 646d1d51f48625346594f95992e1db905cc7e86a Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:43:41 -0600 Subject: [PATCH 23/67] correct comment --- src/server/kbn_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 504ea2f4091ae9..9c3115b2e99f19 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -49,7 +49,7 @@ export default class KbnServer { // writes pid file pidMixin, - // find plugins and set this.plugins + // find plugins and set this.plugins and this.pluginSpecs Plugins.scanMixin, // tell the config we are done loading plugins From edd6a4d8b99354cd97eae338fab37b7d678cf5bf Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:47:50 -0600 Subject: [PATCH 24/67] [server/waitForPluginsInit] describe queues var --- src/server/plugins/wait_for_plugins_init.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/server/plugins/wait_for_plugins_init.js b/src/server/plugins/wait_for_plugins_init.js index 712f2b9d767ff6..da8bfbe13a108d 100644 --- a/src/server/plugins/wait_for_plugins_init.js +++ b/src/server/plugins/wait_for_plugins_init.js @@ -1,4 +1,9 @@ +/** + * Tracks the individual queue for each kbnServer, rather than attaching + * it to the kbnServer object via a property or something + * @type {WeakMap} + */ const queues = new WeakMap(); export function waitForInitSetupMixin(kbnServer) { From b8bbb3a3d78e7e48302ecb4e2bdec65c25c74e3e Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 01:49:43 -0600 Subject: [PATCH 25/67] [server/plugins] prevent multiple calls to next() by using single then() --- src/server/plugins/lib/plugin.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/plugins/lib/plugin.js b/src/server/plugins/lib/plugin.js index d50096b828c56c..257f16ea790983 100644 --- a/src/server/plugins/lib/plugin.js +++ b/src/server/plugins/lib/plugin.js @@ -68,8 +68,7 @@ export class Plugin { const register = (server, options, next) => { asyncRegister(server, options) - .then(() => next()) - .catch(next); + .then(() => next(), next); }; register.attributes = { name: id, version: version }; From 2e134f6f9b9be5d25632b2c4f3fa2b03e9212c4a Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 13:54:43 -0600 Subject: [PATCH 26/67] [uiApp] remove archaic deprecation warning --- src/ui/ui_apps/ui_app.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/ui/ui_apps/ui_app.js b/src/ui/ui_apps/ui_app.js index 01759b3c1aa18d..0f80d6c35b0e16 100644 --- a/src/ui/ui_apps/ui_app.js +++ b/src/ui/ui_apps/ui_app.js @@ -23,13 +23,6 @@ export class UiApp { throw new Error('Every app must specify an id'); } - if (spec.autoload) { - console.warn( - `"autoload" (used by ${id} app) is no longer a valid app configuration directive.` + - 'Use the \`ui/autoload/*\` modules instead.' - ); - } - this._id = id; this._main = main; this._title = title; From 3dbf0f000a9257056b9805a8d603495f05c460e8 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 15 Nov 2017 14:57:08 -0600 Subject: [PATCH 27/67] [uiApp] tighten up tests --- src/ui/ui_apps/__tests__/ui_app.js | 280 +++++++++++++++++++---------- src/ui/ui_apps/ui_app.js | 47 +++-- 2 files changed, 217 insertions(+), 110 deletions(-) diff --git a/src/ui/ui_apps/__tests__/ui_app.js b/src/ui/ui_apps/__tests__/ui_app.js index 7bc05a8ce0a534..fedcdca6f61087 100644 --- a/src/ui/ui_apps/__tests__/ui_app.js +++ b/src/ui/ui_apps/__tests__/ui_app.js @@ -2,8 +2,9 @@ import sinon from 'sinon'; import expect from 'expect.js'; import { UiApp } from '../ui_app'; +import { UiNavLink } from '../../ui_nav_links'; -function getMockSpec(extraParams) { +function createStubUiAppSpec(extraParams) { return { id: 'uiapp-test', main: 'main.js', @@ -13,109 +14,163 @@ function getMockSpec(extraParams) { icon: 'ui_app_test.svg', linkToLastSubUrl: true, hidden: false, - listed: null, + listed: false, templateName: 'ui_app_test', + uses: [ + 'visTypes', + 'chromeNavControls', + 'hacks', + ], + injectVars() { + return { foo: 'bar' }; + }, ...extraParams }; } -function getMockKbnServer() { +function createStubKbnServer() { return { plugins: [], - uiExports: {}, + uiExports: { + appExtensions: { + hacks: [ + 'plugins/foo/hack' + ] + } + }, config: { get: sinon.stub() .withArgs('server.basePath') .returns('') - } + }, + server: {} }; } +function createUiApp(spec = createStubUiAppSpec(), kbnServer = createStubKbnServer()) { + return new UiApp(kbnServer, spec); +} + describe('UiApp', () => { describe('constructor', () => { it('throws an exception if an ID is not given', () => { - function newAppMissingID() { - const spec = {}; // should have id property - const kbnServer = getMockKbnServer(); - const newApp = new UiApp(kbnServer, spec); - return newApp; - } - expect(newAppMissingID).to.throwException(); + const spec = {}; // should have id property + expect(() => createUiApp(spec)).to.throwException(); }); describe('defaults', () => { const spec = { id: 'uiapp-test-defaults' }; - const kbnServer = getMockKbnServer(); - let newApp; - beforeEach(() => { - newApp = new UiApp(kbnServer, spec); - }); + const app = createUiApp(spec); it('has the ID from the spec', () => { - expect(newApp.getId()).to.be(spec.id); + expect(app.getId()).to.be(spec.id); }); - it('has a navLink', () => { - expect(!!newApp.getNavLink()).to.be(true); + it('has no plugin ID', () => { + expect(app.getPluginId()).to.be(undefined); }); it('has a default template name of ui_app', () => { - expect(newApp.getTemplateName()).to.be('ui_app'); + expect(app.getTemplateName()).to.be('ui_app'); }); - describe('uiApp.getInjectedVars()', () => { - it('returns undefined by default', () => { - expect(newApp.getInjectedVars()).to.be(undefined); - }); + it('is not hidden', () => { + expect(app.isHidden()).to.be(false); + }); + + it('is listed', () => { + expect(app.isListed()).to.be(true); + }); + + it('has a navLink', () => { + expect(app.getNavLink()).to.be.a(UiNavLink); }); - describe('JSON representation', () => { - it('has defaults', () => { - expect(JSON.parse(JSON.stringify(newApp))).to.eql({ - id: spec.id, - navLink: { - id: 'uiapp-test-defaults', - order: 0, - url: '/app/uiapp-test-defaults', - subUrlBase: '/app/uiapp-test-defaults', - linkToLastSubUrl: true, - hidden: false, - disabled: false, - tooltip: '' - }, - }); + it('has no injected vars', () => { + expect(app.getInjectedVars()).to.be(undefined); + }); + + it('has an empty modules list', () => { + expect(app.getModules()).to.eql([]); + }); + + it('has a mostly empty JSON representation', () => { + expect(JSON.parse(JSON.stringify(app))).to.eql({ + id: spec.id, + navLink: { + id: 'uiapp-test-defaults', + order: 0, + url: '/app/uiapp-test-defaults', + subUrlBase: '/app/uiapp-test-defaults', + linkToLastSubUrl: true, + hidden: false, + disabled: false, + tooltip: '' + }, }); }); }); describe('mock spec', () => { - describe('JSON representation', () => { - it('has defaults and values from spec', () => { - const kbnServer = getMockKbnServer(); - const spec = getMockSpec(); - const uiApp = new UiApp(kbnServer, spec); - - expect(JSON.parse(JSON.stringify(uiApp))).to.eql({ - id: spec.id, - title: spec.title, - description: spec.description, - icon: spec.icon, - main: spec.main, - linkToLastSubUrl: spec.linkToLastSubUrl, - navLink: { - id: 'uiapp-test', - title: 'UIApp Test', - order: 9000, - url: '/app/uiapp-test', - subUrlBase: '/app/uiapp-test', - description: 'Test of UI App Constructor', - icon: 'ui_app_test.svg', - linkToLastSubUrl: true, - hidden: false, - disabled: false, - tooltip: '' - }, - }); + const spec = createStubUiAppSpec(); + const app = createUiApp(spec); + + it('has the ID from the spec', () => { + expect(app.getId()).to.be(spec.id); + }); + + it('has no plugin ID', () => { + expect(app.getPluginId()).to.be(undefined); + }); + + it('uses the specs template', () => { + expect(app.getTemplateName()).to.be(spec.templateName); + }); + + it('is not hidden', () => { + expect(app.isHidden()).to.be(false); + }); + + it('is also not listed', () => { + expect(app.isListed()).to.be(false); + }); + + it('has no navLink', () => { + expect(app.getNavLink()).to.be(undefined); + }); + + it('has injected vars', () => { + expect(app.getInjectedVars()).to.eql({ foo: 'bar' }); + }); + + it('includes main and hack modules', () => { + expect(app.getModules()).to.eql([ + 'main.js', + 'plugins/foo/hack' + ]); + }); + + it('has spec values in JSON representation', () => { + expect(JSON.parse(JSON.stringify(app))).to.eql({ + id: spec.id, + title: spec.title, + description: spec.description, + icon: spec.icon, + main: spec.main, + linkToLastSubUrl: spec.linkToLastSubUrl, + navLink: { + id: 'uiapp-test', + title: 'UIApp Test', + order: 9000, + url: '/app/uiapp-test', + subUrlBase: '/app/uiapp-test', + description: 'Test of UI App Constructor', + icon: 'ui_app_test.svg', + linkToLastSubUrl: true, + hidden: false, + disabled: false, + tooltip: '' + }, }); }); }); @@ -128,7 +183,7 @@ describe('UiApp', () => { describe('hidden flag', () => { describe('is cast to boolean value', () => { it('when undefined', () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', }; @@ -137,7 +192,7 @@ describe('UiApp', () => { }); it('when null', () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', hidden: null, @@ -147,7 +202,7 @@ describe('UiApp', () => { }); it('when 0', () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', hidden: 0, @@ -157,7 +212,7 @@ describe('UiApp', () => { }); it('when true', () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', hidden: true, @@ -167,7 +222,7 @@ describe('UiApp', () => { }); it('when 1', () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', hidden: 1, @@ -181,7 +236,7 @@ describe('UiApp', () => { describe('listed flag', () => { describe('defaults to the opposite value of hidden', () => { it(`when it's null and hidden is true`, () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', hidden: true, @@ -192,7 +247,7 @@ describe('UiApp', () => { }); it(`when it's null and hidden is false`, () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', hidden: false, @@ -203,7 +258,7 @@ describe('UiApp', () => { }); it(`when it's undefined and hidden is false`, () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', hidden: false, @@ -213,7 +268,7 @@ describe('UiApp', () => { }); it(`when it's undefined and hidden is true`, () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', hidden: true, @@ -224,7 +279,7 @@ describe('UiApp', () => { }); it(`is set to true when it's passed as true`, () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', listed: true, @@ -234,7 +289,7 @@ describe('UiApp', () => { }); it(`is set to false when it's passed as false`, () => { - const kbnServer = getMockKbnServer(); + const kbnServer = createStubKbnServer(); const spec = { id: 'uiapp-test', listed: false, @@ -245,25 +300,58 @@ describe('UiApp', () => { }); }); - describe('getModules', () => { - it('gets modules from kbnServer', () => { - const spec = getMockSpec(); - const kbnServer = { - ...getMockKbnServer(), - uiExports: { - appExtensions: { - chromeNavControls: [ 'plugins/ui_app_test/views/nav_control' ], - hacks: [ 'plugins/ui_app_test/hacks/init' ] + describe('pluginId', () => { + describe('not specified', () => { + it('passes the root server and undefined for plugin/optoins to injectVars()', () => { + const injectVars = sinon.stub(); + const kbnServer = createStubKbnServer(); + createUiApp(createStubUiAppSpec({ injectVars }), kbnServer).getInjectedVars(); + + sinon.assert.calledOnce(injectVars); + sinon.assert.calledOn(injectVars, sinon.match.same(undefined)); + sinon.assert.calledWithExactly( + injectVars, + // server arg, uses root server because there is no plugin + sinon.match.same(kbnServer.server), + // options is undefined because there is no plugin + sinon.match.same(undefined) + ); + }); + }); + describe('matches a kbnServer plugin', () => { + it('passes the plugin/server/options from the plugin to injectVars()', () => { + const server = {}; + const options = {}; + const plugin = { + id: 'test plugin id', + getServer() { + return server; + }, + getOptions() { + return options; } - } - }; - - const newApp = new UiApp(kbnServer, spec); - expect(newApp.getModules()).to.eql([ - 'main.js', - 'plugins/ui_app_test/views/nav_control', - 'plugins/ui_app_test/hacks/init' - ]); + }; + + const kbnServer = createStubKbnServer(); + kbnServer.plugins.push(plugin); + + const injectVars = sinon.stub(); + const spec = createStubUiAppSpec({ pluginId: plugin.id, injectVars }); + createUiApp(spec, kbnServer).getInjectedVars(); + + sinon.assert.calledOnce(injectVars); + sinon.assert.calledOn(injectVars, sinon.match.same(plugin)); + sinon.assert.calledWithExactly(injectVars, sinon.match.same(server), sinon.match.same(options)); + }); + }); + describe('does not match a kbnServer plugin', () => { + it('throws an error at instantiation', () => { + expect(() => { + createUiApp(createStubUiAppSpec({ pluginId: 'foo' })); + }).to.throwException((error) => { + expect(error.message).to.match(/Unknown plugin id/); + }); + }); }); }); }); diff --git a/src/ui/ui_apps/ui_app.js b/src/ui/ui_apps/ui_app.js index 0f80d6c35b0e16..7c8c6e70a494aa 100644 --- a/src/ui/ui_apps/ui_app.js +++ b/src/ui/ui_apps/ui_app.js @@ -34,24 +34,18 @@ export class UiApp { this._listed = listed; this._templateName = templateName; this._url = url; + this._injectedVarsProvider = injectVars; this._pluginId = pluginId; + this._kbnServer = kbnServer; - const plugin = kbnServer.plugins - .find(plugin => plugin.id === this._pluginId); - - this._injectVars = () => { - if (!injectVars) { - return; - } - - const server = plugin.getServer(); - const options = plugin.getOptions(); - return injectVars.call(plugin, server, options); - }; + if (this._pluginId && !this._getPlugin()) { + throw new Error(`Unknown plugin id "${this._pluginId}"`); + } const { appExtensions = [] } = kbnServer.uiExports; this._modules = [] .concat(this._main, ...uses.map(type => appExtensions[type] || [])) + .filter(Boolean) .reduce((modules, item) => ( modules.includes(item) ? modules @@ -79,7 +73,8 @@ export class UiApp { } getPluginId() { - return this._pluginId; + const plugin = this._getPlugin(); + return plugin ? plugin.id : undefined; } getTemplateName() { @@ -104,13 +99,37 @@ export class UiApp { } getInjectedVars() { - return this._injectVars(); + const provider = this._injectedVarsProvider; + const plugin = this._getPlugin(); + + if (!provider) { + return; + } + + return provider.call( + plugin, + plugin + ? plugin.getServer() + : this._kbnServer.server, + plugin + ? plugin.getOptions() + : undefined + ); } getModules() { return this._modules; } + _getPlugin() { + const pluginId = this._pluginId; + const { plugins } = this._kbnServer; + + return pluginId + ? plugins.find(plugin => plugin.id === pluginId) + : undefined; + } + toJSON() { return { id: this._id, From c7a4e5d16ca6bb0f45dba22aa41122f3d9f4c2f6 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 17 Nov 2017 10:47:41 -0600 Subject: [PATCH 28/67] [pluginDiscovery/errors] remove $ from symbol var --- src/plugin_discovery/errors.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/plugin_discovery/errors.js b/src/plugin_discovery/errors.js index 7315e1aea4cb3b..65efa02c93aecf 100644 --- a/src/plugin_discovery/errors.js +++ b/src/plugin_discovery/errors.js @@ -1,5 +1,5 @@ -const $code = Symbol('FindErrorCode'); +const errorCodeProperty = Symbol('pluginDiscovery/errorCode'); /** * Thrown when reading a plugin directory fails, wraps failure @@ -7,12 +7,12 @@ const $code = Symbol('FindErrorCode'); */ const ERROR_INVALID_DIRECTORY = 'ERROR_INVALID_DIRECTORY'; export function createInvalidDirectoryError(sourceError, path) { - sourceError[$code] = ERROR_INVALID_DIRECTORY; + sourceError[errorCodeProperty] = ERROR_INVALID_DIRECTORY; sourceError.path = path; return sourceError; } export function isInvalidDirectoryError(error) { - return error && error[$code] === ERROR_INVALID_DIRECTORY; + return error && error[errorCodeProperty] === ERROR_INVALID_DIRECTORY; } @@ -24,12 +24,12 @@ export function isInvalidDirectoryError(error) { const ERROR_INVALID_PACK = 'ERROR_INVALID_PACK'; export function createInvalidPackError(path, reason) { const error = new Error(`PluginPack${path ? ` at "${path}"` : ''} ${reason}`); - error[$code] = ERROR_INVALID_PACK; + error[errorCodeProperty] = ERROR_INVALID_PACK; error.path = path; return error; } export function isInvalidPackError(error) { - return error && error[$code] === ERROR_INVALID_PACK; + return error && error[errorCodeProperty] === ERROR_INVALID_PACK; } /** @@ -39,12 +39,12 @@ export function isInvalidPackError(error) { const ERROR_INVALID_PLUGIN = 'ERROR_INVALID_PLUGIN'; export function createInvalidPluginError(spec, reason) { const error = new Error(`Plugin from ${spec.getId()} from ${spec.getPack().getPath()} is invalid because ${reason}`); - error[$code] = ERROR_INVALID_PLUGIN; + error[errorCodeProperty] = ERROR_INVALID_PLUGIN; error.spec = spec; return error; } export function isInvalidPluginError(error) { - return error && error[$code] === ERROR_INVALID_PLUGIN; + return error && error[errorCodeProperty] === ERROR_INVALID_PLUGIN; } /** @@ -54,10 +54,10 @@ export function isInvalidPluginError(error) { const ERROR_INCOMPATIBLE_PLUGIN_VERSION = 'ERROR_INCOMPATIBLE_PLUGIN_VERSION'; export function createIncompatiblePluginVersionError(spec) { const error = new Error(`Plugin ${spec.getId()} is only compatible with Kibana version ${spec.getExpectedKibanaVersion()}`); - error[$code] = ERROR_INCOMPATIBLE_PLUGIN_VERSION; + error[errorCodeProperty] = ERROR_INCOMPATIBLE_PLUGIN_VERSION; error.spec = spec; return error; } export function isIncompatiblePluginVersionError(error) { - return error && error[$code] === ERROR_INCOMPATIBLE_PLUGIN_VERSION; + return error && error[errorCodeProperty] === ERROR_INCOMPATIBLE_PLUGIN_VERSION; } From ff3b81531a8ba518c0a2270ea0c915685a9377e5 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 17 Nov 2017 10:49:09 -0600 Subject: [PATCH 29/67] [pluginDiscovery/reduceExportSpecs] update docs --- src/plugin_discovery/plugin_exports/reduce_export_specs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugin_discovery/plugin_exports/reduce_export_specs.js b/src/plugin_discovery/plugin_exports/reduce_export_specs.js index 936c9c4daced31..24b02056de5c81 100644 --- a/src/plugin_discovery/plugin_exports/reduce_export_specs.js +++ b/src/plugin_discovery/plugin_exports/reduce_export_specs.js @@ -2,8 +2,8 @@ * Combine the exportSpecs from a list of pluginSpecs * by calling the reducers for each export type * @param {Array} pluginSpecs - * @param {Object} exportTypes - * @param {Object} reducers + * @param {Object} [defaults={}] * @return {Object} */ export function reduceExportSpecs(pluginSpecs, reducers, defaults = {}) { From f30dba38b8e7b3487f927071e54ea9f7dd711dc5 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 17 Nov 2017 10:52:21 -0600 Subject: [PATCH 30/67] [pluginDiscovery/findPluginSpecs] rightVersion -> isRightVersion --- src/plugin_discovery/find_plugin_specs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugin_discovery/find_plugin_specs.js b/src/plugin_discovery/find_plugin_specs.js index 491b784997e9dd..c081f05ec16f77 100644 --- a/src/plugin_discovery/find_plugin_specs.js +++ b/src/plugin_discovery/find_plugin_specs.js @@ -70,14 +70,14 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { // extend the config with all plugins before determining enabled status .let(waitForComplete) .map(({ spec, deprecations }) => { - const rightVersion = spec.isVersionCompatible(config.get('pkg.version')); - const enabled = rightVersion && spec.isEnabled(config); + const isRightVersion = spec.isVersionCompatible(config.get('pkg.version')); + const enabled = isRightVersion && spec.isEnabled(config); return { spec, deprecations, enabledSpecs: enabled ? [spec] : [], disabledSpecs: enabled ? [] : [spec], - invalidVersionSpecs: rightVersion ? [] : [spec], + invalidVersionSpecs: isRightVersion ? [] : [spec], }; }) // determin which plugins are disabled before actually removing things from the config From 893cc4a77ff8c5d20d90f4914d403c531bbd070b Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 17 Nov 2017 10:52:45 -0600 Subject: [PATCH 31/67] [pluginDiscovery/findPluginSpecs] fix typos --- src/plugin_discovery/find_plugin_specs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugin_discovery/find_plugin_specs.js b/src/plugin_discovery/find_plugin_specs.js index c081f05ec16f77..f2412f2ef37579 100644 --- a/src/plugin_discovery/find_plugin_specs.js +++ b/src/plugin_discovery/find_plugin_specs.js @@ -80,7 +80,7 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { invalidVersionSpecs: isRightVersion ? [] : [spec], }; }) - // determin which plugins are disabled before actually removing things from the config + // determine which plugins are disabled before actually removing things from the config .let(waitForComplete) .do(result => { for (const spec of result.disabledSpecs) { @@ -108,7 +108,7 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { isInvalidPackError(result.error) ? [result.error] : [] )), - // { spec, message } objects produces when transforming deprecated + // { spec, message } objects produced when transforming deprecated // settings for a plugin spec deprecation$: extendConfig$ .mergeMap(result => result.deprecations), From 7a47910c0a303b44394d34bdbb8ca5789e7751ab Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 17 Nov 2017 13:23:09 -0600 Subject: [PATCH 32/67] [uiApps/getById] use Map() rather than memoize --- src/ui/ui_apps/ui_apps_mixin.js | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/ui/ui_apps/ui_apps_mixin.js b/src/ui/ui_apps/ui_apps_mixin.js index 7f60727bd36e59..a3d20e9fbffb1d 100644 --- a/src/ui/ui_apps/ui_apps_mixin.js +++ b/src/ui/ui_apps/ui_apps_mixin.js @@ -1,13 +1,13 @@ -import { memoize } from 'lodash'; - import { UiApp } from './ui_app'; export function uiAppsMixin(kbnServer, server) { const { uiAppSpecs = [] } = kbnServer.uiExports; const existingIds = new Set(); + const appsById = new Map(); + const hiddenAppsById = new Map(); - kbnServer.uiApps = uiAppSpecs.map(spec => { + kbnServer.uiApps = uiAppSpecs.map((spec) => { const app = new UiApp(kbnServer, spec); const id = app.getId(); @@ -17,22 +17,16 @@ export function uiAppsMixin(kbnServer, server) { throw new Error(`Unable to create two apps with the id ${id}.`); } + if (app.isHidden()) { + hiddenAppsById.set(id, app); + } else { + appsById.set(id, app); + } + return app; }); - server.decorate('server', 'getAllUiApps', () => ( - kbnServer.uiApps.slice(0) - )); - - server.decorate('server', 'getUiAppById', memoize(id => ( - kbnServer.uiApps.find(uiApp => ( - uiApp.getId() === id && !uiApp.isHidden() - )) - ))); - - server.decorate('server', 'getHiddenUiAppById', memoize(id => ( - kbnServer.uiApps.find(uiApp => ( - uiApp.getId() === id && uiApp.isHidden() - )) - ))); + server.decorate('server', 'getAllUiApps', () => kbnServer.uiApps.slice(0)); + server.decorate('server', 'getUiAppById', id => appsById.get(id)); + server.decorate('server', 'getHiddenUiAppById', id => hiddenAppsById.get(id)); } From 424eae5fa896b6b6f40cc5fb09785611772b8aa2 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 20 Nov 2017 17:02:06 -0700 Subject: [PATCH 33/67] save --- src/core_plugins/kibana/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core_plugins/kibana/index.js b/src/core_plugins/kibana/index.js index 2b35a4f4afb41a..7b24b12dc048d1 100644 --- a/src/core_plugins/kibana/index.js +++ b/src/core_plugins/kibana/index.js @@ -40,10 +40,6 @@ export default function (kibana) { 'plugins/kibana/discover/saved_searches/saved_search_register', 'plugins/kibana/dashboard/saved_dashboard/saved_dashboard_register', ], - embeddableHandlers: [ - 'plugins/kibana/visualize/embeddable/visualize_embeddable_handler_provider', - 'plugins/kibana/discover/embeddable/search_embeddable_handler_provider', - ], app: { id: 'kibana', title: 'Kibana', From e8187045fa75b8ff988d2b9885d03882a3d9a487 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 29 Nov 2017 16:18:37 -0700 Subject: [PATCH 34/67] [savedObjects/mappings] use uiExports.savedObjectMappings --- src/server/kbn_server.js | 6 +-- src/server/mappings/index_mappings.js | 42 +++++++++---------- .../mappings/kibana_index_mappings_mixin.js | 21 +++------- .../ui_export_types/saved_object_mappings.js | 11 ++++- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 9c3115b2e99f19..1167823d425003 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -55,13 +55,13 @@ export default class KbnServer { // tell the config we are done loading plugins configCompleteMixin, - // setup kbnServer.mappings and server.getKibanaIndexMappingsDsl() - kibanaIndexMappingsMixin, - // setup this.uiExports and this.uiBundles uiMixin, indexPatternsMixin, + // setup server.getKibanaIndexMappingsDsl() + kibanaIndexMappingsMixin, + // setup saved object routes savedObjectsMixin, diff --git a/src/server/mappings/index_mappings.js b/src/server/mappings/index_mappings.js index 26aaa9e7c5ad2b..5f767b52ff686b 100644 --- a/src/server/mappings/index_mappings.js +++ b/src/server/mappings/index_mappings.js @@ -11,7 +11,7 @@ const DEFAULT_INITIAL_DSL = { }; export class IndexMappings { - constructor(initialDsl = DEFAULT_INITIAL_DSL) { + constructor(initialDsl = DEFAULT_INITIAL_DSL, mappingExtensions = []) { this._dsl = cloneDeep(initialDsl); if (!isPlainObject(this._dsl)) { throw new TypeError('initial mapping must be an object'); @@ -20,34 +20,34 @@ export class IndexMappings { // ensure that we have a properties object in the dsl // and that the dsl can be parsed with getRootProperties() and kin this._setProperties(getRootProperties(this._dsl) || {}); - } - - getDsl() { - return cloneDeep(this._dsl); - } - addRootProperties(newProperties, options = {}) { - const { plugin } = options; - const rootProperties = getRootProperties(this._dsl); + // extend this._dsl with each extension (which currently come from uiExports.savedObjectMappings) + mappingExtensions.forEach(({ properties, pluginId }) => { + const rootProperties = getRootProperties(this._dsl); - const conflicts = Object.keys(newProperties) - .filter(key => rootProperties.hasOwnProperty(key)); + const conflicts = Object.keys(properties) + .filter(key => rootProperties.hasOwnProperty(key)); - if (conflicts.length) { - const props = formatListAsProse(conflicts); - const owner = plugin ? `registered by plugin ${plugin} ` : ''; - throw new Error( - `Mappings for ${props} ${owner}have already been defined` - ); - } + if (conflicts.length) { + const props = formatListAsProse(conflicts); + const owner = pluginId ? `registered by plugin ${pluginId} ` : ''; + throw new Error( + `Mappings for ${props} ${owner}have already been defined` + ); + } - this._setProperties({ - ...rootProperties, - ...newProperties + this._setProperties({ + ...rootProperties, + ...properties + }); }); } + getDsl() { + return cloneDeep(this._dsl); + } + _setProperties(newProperties) { const rootType = getRootType(this._dsl); this._dsl = { diff --git a/src/server/mappings/kibana_index_mappings_mixin.js b/src/server/mappings/kibana_index_mappings_mixin.js index e1a169493c950a..606aa2c7e4437c 100644 --- a/src/server/mappings/kibana_index_mappings_mixin.js +++ b/src/server/mappings/kibana_index_mappings_mixin.js @@ -6,7 +6,7 @@ import { IndexMappings } from './index_mappings'; * and timelion plugins for examples. * @type {EsMappingDsl} */ -const BASE_KIBANA_INDEX_MAPPINGS_DSL = { +const BASE_SAVED_OBJECT_MAPPINGS = { doc: { dynamic: 'strict', properties: { @@ -29,19 +29,10 @@ const BASE_KIBANA_INDEX_MAPPINGS_DSL = { }; export function kibanaIndexMappingsMixin(kbnServer, server) { - /** - * Stores the current mappings that we expect to find in the Kibana - * index. Using `kbnServer.mappings.addRootProperties()` the UiExports - * class extends these mappings based on `mappings` ui export specs. - * - * Application code should not access this object, and instead should - * use `server.getKibanaIndexMappingsDsl()` from below, mixed with the - * helpers exposed by this module, to interact with the mappings via - * their DSL. - * - * @type {IndexMappings} - */ - kbnServer.mappings = new IndexMappings(BASE_KIBANA_INDEX_MAPPINGS_DSL); + const mappings = new IndexMappings( + BASE_SAVED_OBJECT_MAPPINGS, + kbnServer.uiExports.savedObjectMappings + ); /** * Get the mappings dsl that we expect to see in the @@ -57,6 +48,6 @@ export function kibanaIndexMappingsMixin(kbnServer, server) { * @returns {EsMappingDsl} */ server.decorate('server', 'getKibanaIndexMappingsDsl', () => { - return kbnServer.mappings.getDsl(); + return mappings.getDsl(); }); } diff --git a/src/ui/ui_exports/ui_export_types/saved_object_mappings.js b/src/ui/ui_exports/ui_export_types/saved_object_mappings.js index 6b9cf63ec95d6c..ebbc9ef18b100d 100644 --- a/src/ui/ui_exports/ui_export_types/saved_object_mappings.js +++ b/src/ui/ui_exports/ui_export_types/saved_object_mappings.js @@ -1,5 +1,12 @@ import { concat } from './reduce'; -import { wrap, debug } from './modify_reduce'; +import { alias, mapSpec, wrap } from './modify_reduce'; // mapping types -export const mappings = wrap(debug, concat); +export const mappings = wrap( + alias('savedObjectMappings'), + mapSpec((spec, type, pluginSpec) => ({ + pluginId: pluginSpec.getId(), + properties: spec + })), + concat +); From 2cbf032ead81553d7daeed9d18c63cad32e6fec8 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 30 Nov 2017 10:53:48 -0700 Subject: [PATCH 35/67] [server/mapping/indexMapping] update tests, addRootProperties method removed --- .../mappings/__tests__/index_mappings.js | 75 ++++++++++++------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/src/server/mappings/__tests__/index_mappings.js b/src/server/mappings/__tests__/index_mappings.js index 999c0eb0d5b3c2..981b66e673084b 100644 --- a/src/server/mappings/__tests__/index_mappings.js +++ b/src/server/mappings/__tests__/index_mappings.js @@ -64,32 +64,26 @@ describe('server/mapping/index_mapping', function () { } }); }); - }); - - describe('#getDsl()', () => { - // tests are light because this method is used all over these tests - it('returns mapping as es dsl', function () { - const mapping = new IndexMappings(); - expect(mapping.getDsl()).to.be.an('object'); - }); - }); - describe('#addRootProperties()', () => { - it('extends the properties of the root type', () => { - const mapping = new IndexMappings({ + it('accepts an array of new extensions that will be added to the mapping', () => { + const initialMapping = { x: { properties: {} } - }); - - mapping.addRootProperties({ - y: { + }; + const extensions = [ + { properties: { - z: { - type: 'text' + y: { + properties: { + z: { + type: 'text' + } + } } } } - }); + ]; + const mapping = new IndexMappings(initialMapping, extensions); expect(mapping.getDsl()).to.eql({ x: { properties: { @@ -105,24 +99,47 @@ describe('server/mapping/index_mapping', function () { }); }); - it('throws if any property is conflicting', () => { - const props = { foo: 'bar' }; - const mapping = new IndexMappings({ - root: { properties: props } - }); + it('throws if any of the new properties conflict', () => { + const initialMapping = { + root: { properties: { foo: 'bar' } } + }; + const extensions = [ + { + properties: { + foo: 'bar' + } + } + ]; expect(() => { - mapping.addRootProperties(props); + new IndexMappings(initialMapping, extensions); }).to.throwException(/foo/); }); - it('includes the plugin option in the error message when specified', () => { - const props = { foo: 'bar' }; - const mapping = new IndexMappings({ root: { properties: props } }); + it('includes the pluginId from the extension in the error message if defined', () => { + const initialMapping = { + root: { properties: { foo: 'bar' } } + }; + const extensions = [ + { + pluginId: 'abc123', + properties: { + foo: 'bar' + } + } + ]; expect(() => { - mapping.addRootProperties(props, { plugin: 'abc123' }); + new IndexMappings(initialMapping, extensions); }).to.throwException(/plugin abc123/); }); }); + + describe('#getDsl()', () => { + // tests are light because this method is used all over these tests + it('returns mapping as es dsl', function () { + const mapping = new IndexMappings(); + expect(mapping.getDsl()).to.be.an('object'); + }); + }); }); From cc3fc83f4aa49f2bda99be6c5d5c914cfa86f0d3 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 30 Nov 2017 18:17:31 -0700 Subject: [PATCH 36/67] [uiExports] "embeddableHandlers" -> "embeddableFactories" --- src/ui/ui_exports/ui_export_defaults.js | 4 ++++ src/ui/ui_exports/ui_export_types/index.js | 2 +- src/ui/ui_exports/ui_export_types/ui_app_extensions.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui/ui_exports/ui_export_defaults.js b/src/ui/ui_exports/ui_export_defaults.js index d911c85ff4380d..bd96d12fccd678 100644 --- a/src/ui/ui_exports/ui_export_defaults.js +++ b/src/ui/ui_exports/ui_export_defaults.js @@ -37,5 +37,9 @@ export const UI_EXPORT_DEFAULTS = { visEditorTypes: [ 'ui/vis/editors/default/default', ], + embeddableFactories: [ + 'plugins/kibana/visualize/embeddable/visualize_embeddable_factory_provider', + 'plugins/kibana/discover/embeddable/search_embeddable_factory_provider', + ] }, }; diff --git a/src/ui/ui_exports/ui_export_types/index.js b/src/ui/ui_exports/ui_export_types/index.js index 53f080720f3d10..55a35d54c3795a 100644 --- a/src/ui/ui_exports/ui_export_types/index.js +++ b/src/ui/ui_exports/ui_export_types/index.js @@ -18,7 +18,7 @@ export { visRequestHandlers, visEditorTypes, savedObjectTypes, - embeddableHandlers, + embeddableFactories, fieldFormats, fieldFormatEditors, spyModes, diff --git a/src/ui/ui_exports/ui_export_types/ui_app_extensions.js b/src/ui/ui_exports/ui_export_types/ui_app_extensions.js index daba06506b575a..8e493df3103f5e 100644 --- a/src/ui/ui_exports/ui_export_types/ui_app_extensions.js +++ b/src/ui/ui_exports/ui_export_types/ui_app_extensions.js @@ -19,7 +19,7 @@ export const visResponseHandlers = appExtension; export const visRequestHandlers = appExtension; export const visEditorTypes = appExtension; export const savedObjectTypes = appExtension; -export const embeddableHandlers = appExtension; +export const embeddableFactories = appExtension; export const fieldFormats = appExtension; export const fieldFormatEditors = appExtension; export const spyModes = appExtension; From dee11987c25c021917f89fba06997d3174a622a4 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 17:40:58 -0700 Subject: [PATCH 37/67] [pluginDiscovery] fix pluralization of invalidVersionSpec$ --- src/plugin_discovery/find_plugin_specs.js | 2 +- src/server/plugins/scan_mixin.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugin_discovery/find_plugin_specs.js b/src/plugin_discovery/find_plugin_specs.js index f2412f2ef37579..7edae71c2c018b 100644 --- a/src/plugin_discovery/find_plugin_specs.js +++ b/src/plugin_discovery/find_plugin_specs.js @@ -128,7 +128,7 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { .mergeMap(result => result.disabledSpecs), // all PluginSpec objects that were disabled because their version was incompatible - invalidVersionSpecs$: extendConfig$ + invalidVersionSpec$: extendConfig$ .mergeMap(result => result.invalidVersionSpecs), }; } diff --git a/src/server/plugins/scan_mixin.js b/src/server/plugins/scan_mixin.js index 67b09525682d71..fb5d8960a11478 100644 --- a/src/server/plugins/scan_mixin.js +++ b/src/server/plugins/scan_mixin.js @@ -9,7 +9,7 @@ export async function scanMixin(kbnServer, server, config) { invalidDirectoryError$, invalidPackError$, deprecation$, - invalidVersionSpecs$, + invalidVersionSpec$, spec$, } = findPluginSpecs(kbnServer.settings, config); @@ -36,7 +36,7 @@ export async function scanMixin(kbnServer, server, config) { }); }), - invalidVersionSpecs$ + invalidVersionSpec$ .map(spec => { const name = spec.getId(); const pluginVersion = spec.getExpectedKibanaVersion(); From b2dc37a11edae20a1007d4c8b65304b8f179d85d Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 18:04:11 -0700 Subject: [PATCH 38/67] [pluginDiscover] add README --- src/plugin_discovery/README.md | 113 +++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/plugin_discovery/README.md diff --git a/src/plugin_discovery/README.md b/src/plugin_discovery/README.md new file mode 100644 index 00000000000000..67ba180de35022 --- /dev/null +++ b/src/plugin_discovery/README.md @@ -0,0 +1,113 @@ +# Plugin Discovery + +The plugin discovery module defines the core plugin loading logic used by the Kibana server. It exports functions for + + +## `findPluginSpecs(settings, [config])` + +Finds [`PluginSpec`](PluginSpec) objects + +### params + - `settings`: the same settings object accepted by [`KbnServer`](KbnServer) + - `[config]`: Optional - a [`Config`](Config) service. Using this param causes `findPluginSpecs()` to modify `config`'s schema to support the configuration for each discovered [`PluginSpec`](PluginSpec). If you can, please use the [`Config`](Config) service produced by `extendedConfig$` rather than passing in an existing service so that `findPluginSpecs()` is side-effect free. + +### return value + +`findPluginSpecs()` returns an object of Observables which produce values at different parts of the process. Since the Observables are all aware of their own dependencies you can subscribe to any combination (within the same tick) and only the necessary plugin logic will be executed. + +If you *never* subscribe to any of the Observables then plugin discovery won't actually run. + + - `pack$`: emits every [`PluginPack`](PluginPack) found + - `invalidDirectoryError$: Observable`: emits [`InvalidDirectoryError`](Errors)s caused by `settings.plugins.scanDirs` values that don't point to actual directories. `findPluginSpecs()` will not abort when this error is encountered. + - `invalidPackError$: Observable`: emits [`InvalidPackError`](Errors)s caused by children of `settings.plugins.scanDirs` or `settings.plugins.paths` values which don't meet the requirements of a [`PluginPack`](PluginPack) (probably missing a `package.json`). `findPluginSpecs()` will not abort when this error is encountered. + - `deprecation$: Observable`: emits deprecation warnings that are produces when reading each [`PluginPack`](PluginPack)'s configuration + - `extendedConfig$: Observable`: emits the [`Config`](Config) service that was passed to `findPluginSpecs()` (or created internally if none was passed) after it has been extended with the configuration from each plugin + - `spec$: Observable`: emits every *enabled* [`PluginSpec`](PluginSpec) defined by the discovered [`PluginPack`](PluginPack)s + - `disabledSpecs$: Observable`: emits every *disabled* [`PluginSpec`](PluginSpec) defined by the discovered [`PluginPack`](PluginPack)s + - `invalidVersionSpec$: Observable`: emits every [`PluginSpec`](PluginSpec) who's required kibana version does not match the version exposed by `config.get('pkg.version')` + +### example + +```js +import { readYamlConfig } from 'src/cli/serve/read_yaml_config' +const settings = readYamlConfig('config/kibana.yml') + +const { + pack$, + invalidDirectoryError$, + invalidPackError$, + deprecation$, + extendedConfig$, + spec$, + disabledSpecs$, + invalidVersionSpec$, +} = findPluginSpecs(settings); + +Observable.merge( + pack$ + .do(pluginPack => console.log('Found plugin pack', pluginPack)), + + invalidDirectoryError$ + .do(error => console.log('Invalid directory error', error)), + + invalidPackError$ + .do(error => console.log('Invalid plugin pack error', error)), + + deprecation$ + .do(msg => console.log('DEPRECATION:', msg)), + + extendedConfig$ + .do(config => console.log('config service extended by plugins', config)), + + spec$ + .do(pluginSpec => console.log('enabled plugin spec found', spec)), + + disabledSpec$ + .do(pluginSpec => console.log('disabled plugin spec found', spec)), + + invalidVersionSpec$ + .do(pluginSpec => console.log('plugin spec with invalid version found', spec)), +) +.toPromise() +.then(() => { + console.log('plugin discovery complete') +}) +.catch((error) => { + console.log('plugin discovery failed', error) +}) + +``` + +## `reduceExportSpecs(pluginSpecs, reducers, [defaults={}])` + +Iterates through every export specification provided by all [`PluginSpec`](PluginSpec) objects passed. and calls the reducer with the signature: + +```js +reducer( + // the result of the previous reducer call, or `defaults` + acc: any, + // the value exported value, found at `uiExports[type]` in the + // PluginSpec config + spec: any, + // the key in `uiExports` where this export was found + type: string, + // the PluginSpec which provided this export + pluginSpec: PluginSpec +) +``` + +## `new PluginPack(options)` class + +Only exported so that `PluginPack` instances can be created in tests and used in place of on-disk plugin fixtures. Use `findPluginSpecs()`, or the cached result of a call to `findPluginSpecs()` (like `kbnServer.pluginSpecs`) any time you might need access to `PluginPack` objects in distributed code. + +### params + + - `options.path`: absolute path to where this plugin pack was found, this is normally a direct child of `./src/core_plugins` or `./plugins` + - `options.pkg`: the parsed `package.json` for this pack, used for defaults in `PluginSpec` objects defined by this pack + - `options.provider`: the default export of the pack, a function which is called with the `PluginSpec` class which should return one or more `PluginSpec` objects. + +[PluginPack]: ./plugin_pack/plugin_pack.js "PluginPath class definition" +[PluginSpec]: ./plugin_spec/plugin_spec.js "PluginSpec class definition" +[Errors]: ./errors.js "PluginDiscover specific error types" +[KbnServer]: ../server/kbn_server.js "KbnServer class definition" +[Config]: ../server/config/config.js "KbnServer/Config class definition" \ No newline at end of file From 92049434a1d071eaca94bce0df43a3a36eb6081e Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 18:37:15 -0700 Subject: [PATCH 39/67] [pluginDiscovery/reduceExportSpecs] don't ignore fasly spec values, just undefined --- src/plugin_discovery/README.md | 2 +- .../plugin_exports/reduce_export_specs.js | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plugin_discovery/README.md b/src/plugin_discovery/README.md index 67ba180de35022..c764fed81b5937 100644 --- a/src/plugin_discovery/README.md +++ b/src/plugin_discovery/README.md @@ -80,7 +80,7 @@ Observable.merge( ## `reduceExportSpecs(pluginSpecs, reducers, [defaults={}])` -Iterates through every export specification provided by all [`PluginSpec`](PluginSpec) objects passed. and calls the reducer with the signature: +Iterates through every export specification provided by all [`PluginSpec`](PluginSpec)s. If an exported specification value is an array each item in the array will be passed to the reducer individually. If the exported specification is `undefined` it will be ignored. The reducer is called with the signature: ```js reducer( diff --git a/src/plugin_discovery/plugin_exports/reduce_export_specs.js b/src/plugin_discovery/plugin_exports/reduce_export_specs.js index 24b02056de5c81..cd3343db8c12ac 100644 --- a/src/plugin_discovery/plugin_exports/reduce_export_specs.js +++ b/src/plugin_discovery/plugin_exports/reduce_export_specs.js @@ -18,7 +18,13 @@ export function reduceExportSpecs(pluginSpecs, reducers, defaults = {}) { throw new Error(`Unknown export type ${type}`); } - const specs = [].concat(specsByType[type] || []); + // convert specs to an array if not already one or + // ignore the spec if it is undefined + const specs = [].concat( + specsByType[type] === undefined + ? [] + : specsByType[type] + ); return specs.reduce((acc, spec) => ( reducer(acc, spec, type, pluginSpec) From a6f14384dce737dd3a576d54946ee9eab5c664d1 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 19:22:23 -0700 Subject: [PATCH 40/67] [ui/exportTypes] use better reducer names --- .../ui_export_types/modify_injected_vars.js | 6 ++--- .../ui_export_types/modify_reduce/wrap.js | 23 +++++++++++++++++-- .../ui_export_types/reduce/concat_values.js | 15 ------------ .../{concat.js => flat_concat_at_type.js} | 6 ++--- .../reduce/flat_concat_values_at_type.js | 15 ++++++++++++ .../ui_export_types/reduce/index.js | 6 ++--- .../reduce/lib/create_type_reducer.js | 13 +++++++++++ .../ui_export_types/reduce/lib/flat_concat.js | 13 +++++++++++ .../ui_export_types/reduce/lib/index.js | 3 +++ .../ui_export_types/reduce/lib/merge_with.js | 22 ++++++++++++++++++ .../ui_export_types/reduce/merge.js | 6 ----- .../ui_export_types/reduce/merge_at_type.js | 6 +++++ .../ui_export_types/reduce/merge_type.js | 10 -------- .../ui_export_types/reduce/unique_assign.js | 12 ---------- .../ui_export_types/saved_object_mappings.js | 4 ++-- .../ui_export_types/ui_app_extensions.js | 6 ++--- src/ui/ui_exports/ui_export_types/ui_apps.js | 6 ++--- src/ui/ui_exports/ui_export_types/ui_i18n.js | 4 ++-- .../ui_export_types/ui_nav_links.js | 6 ++--- .../ui_exports/ui_export_types/ui_settings.js | 4 ++-- src/ui/ui_exports/ui_export_types/unknown.js | 4 ++-- .../ui_export_types/webpack_customizations.js | 8 +++---- 22 files changed, 122 insertions(+), 76 deletions(-) delete mode 100644 src/ui/ui_exports/ui_export_types/reduce/concat_values.js rename src/ui/ui_exports/ui_export_types/reduce/{concat.js => flat_concat_at_type.js} (55%) create mode 100644 src/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js create mode 100644 src/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js create mode 100644 src/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js create mode 100644 src/ui/ui_exports/ui_export_types/reduce/lib/index.js create mode 100644 src/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js delete mode 100644 src/ui/ui_exports/ui_export_types/reduce/merge.js create mode 100644 src/ui/ui_exports/ui_export_types/reduce/merge_at_type.js delete mode 100644 src/ui/ui_exports/ui_export_types/reduce/merge_type.js delete mode 100644 src/ui/ui_exports/ui_export_types/reduce/unique_assign.js diff --git a/src/ui/ui_exports/ui_export_types/modify_injected_vars.js b/src/ui/ui_exports/ui_export_types/modify_injected_vars.js index 364646234a503d..2a26bc98aa4d32 100644 --- a/src/ui/ui_exports/ui_export_types/modify_injected_vars.js +++ b/src/ui/ui_exports/ui_export_types/modify_injected_vars.js @@ -1,7 +1,7 @@ -import { concat } from './reduce'; +import { flatConcatAtType } from './reduce'; import { wrap, alias, mapSpec } from './modify_reduce'; -export const replaceInjectedVars = wrap(alias('injectedVarsReplacers'), concat); +export const replaceInjectedVars = wrap(alias('injectedVarsReplacers'), flatConcatAtType); export const injectDefaultVars = wrap( alias('defaultInjectedVarProviders'), @@ -9,5 +9,5 @@ export const injectDefaultVars = wrap( pluginSpec, fn: spec, })), - concat + flatConcatAtType ); diff --git a/src/ui/ui_exports/ui_export_types/modify_reduce/wrap.js b/src/ui/ui_exports/ui_export_types/modify_reduce/wrap.js index 26e26709dd342a..55a940192bf585 100644 --- a/src/ui/ui_exports/ui_export_types/modify_reduce/wrap.js +++ b/src/ui/ui_exports/ui_export_types/modify_reduce/wrap.js @@ -1,9 +1,28 @@ /** - * Wrap a reducer + * Wrap a function with any number of wrappers. Wrappers + * are functions that take a reducer and return a reducer + * that should be called in its place. The wrappers will + * be called in reverse order for setup and then in the + * order they are defined when the resulting reducer is + * executed. + * + * const reducer = wrap( + * next => (acc) => acc[1] = 'a', + * next => (acc) => acc[1] = 'b', + * next => (acc) => acc[1] = 'c' + * ) + * + * reducer('foo') //=> 'fco' + * * @param {Function} ...wrappers * @param {Function} reducer * @return {Function} */ export function wrap(...args) { - return args.reverse().reduce((reducer, decorate) => decorate(reducer)); + const reducer = args[args.length - 1]; + const wrappers = args.slice(0, -1); + + return wrappers + .reverse() + .reduce((acc, wrapper) => wrapper(acc), reducer); } diff --git a/src/ui/ui_exports/ui_export_types/reduce/concat_values.js b/src/ui/ui_exports/ui_export_types/reduce/concat_values.js deleted file mode 100644 index 5606b58f5e039d..00000000000000 --- a/src/ui/ui_exports/ui_export_types/reduce/concat_values.js +++ /dev/null @@ -1,15 +0,0 @@ -import { assign } from 'lodash'; - -import { mergeType } from './merge_type'; - -/** - * Creates a reducer that merges specs by concatenating the values of - * all keys in accumulator and spec with the same logic as concat - * @param {[type]} initial [description] - * @return {[type]} [description] - */ -export const concatValues = mergeType((objectA, objectB) => ( - assign({}, objectA || {}, objectB || {}, (a, b) => ( - [].concat(a || [], b || []) - )) -)); diff --git a/src/ui/ui_exports/ui_export_types/reduce/concat.js b/src/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js similarity index 55% rename from src/ui/ui_exports/ui_export_types/reduce/concat.js rename to src/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js index 5346a2a09a3268..b141f2fd605cec 100644 --- a/src/ui/ui_exports/ui_export_types/reduce/concat.js +++ b/src/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js @@ -1,4 +1,4 @@ -import { mergeType } from './merge_type'; +import { createTypeReducer, flatConcat } from './lib'; /** * Reducer that merges two values concatenating all values @@ -6,6 +6,4 @@ import { mergeType } from './merge_type'; * @param {Any} [initial] * @return {Function} */ -export const concat = mergeType((a, b) => ( - [].concat(a || [], b || []) -)); +export const flatConcatAtType = createTypeReducer(flatConcat); diff --git a/src/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js b/src/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js new file mode 100644 index 00000000000000..085c2867506473 --- /dev/null +++ b/src/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js @@ -0,0 +1,15 @@ +import { + createTypeReducer, + flatConcat, + mergeWith, +} from './lib'; + +/** + * Reducer that merges specs by concatenating the values of + * all keys in accumulator and spec with the same logic as concat + * @param {[type]} initial [description] + * @return {[type]} [description] + */ +export const flatConcatValuesAtType = createTypeReducer((objectA, objectB) => ( + mergeWith(objectA || {}, objectB || {}, flatConcat) +)); diff --git a/src/ui/ui_exports/ui_export_types/reduce/index.js b/src/ui/ui_exports/ui_export_types/reduce/index.js index f92630d7c0b653..e131fe1c261d6b 100644 --- a/src/ui/ui_exports/ui_export_types/reduce/index.js +++ b/src/ui/ui_exports/ui_export_types/reduce/index.js @@ -1,3 +1,3 @@ -export { concatValues } from './concat_values'; -export { concat } from './concat'; -export { merge } from './merge'; +export { mergeAtType } from './merge_at_type'; +export { flatConcatValuesAtType } from './flat_concat_values_at_type'; +export { flatConcatAtType } from './flat_concat_at_type'; diff --git a/src/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js b/src/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js new file mode 100644 index 00000000000000..c5ff343554e27a --- /dev/null +++ b/src/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js @@ -0,0 +1,13 @@ +/** + * Creates a reducer that reduces the values within `acc[type]` by calling + * reducer with signature: + * + * reducer(acc[type], spec, type, pluginSpec) + * + * @param {Function} reducer + * @return {Function} + */ +export const createTypeReducer = (reducer) => (acc, spec, type, pluginSpec) => ({ + ...acc, + [type]: reducer(acc[type], spec, type, pluginSpec) +}); diff --git a/src/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js b/src/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js new file mode 100644 index 00000000000000..a61c240836d021 --- /dev/null +++ b/src/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js @@ -0,0 +1,13 @@ +/** + * Concatenate two values into a single array, ignoring either + * value if it is undefined and flattening the value if it is an array + * @param {Array|T} a + * @param {Array} b + * @return {Array} + */ +export const flatConcat = (a, b) => ( + [].concat( + a === undefined ? [] : a, + b === undefined ? [] : b + ) +); diff --git a/src/ui/ui_exports/ui_export_types/reduce/lib/index.js b/src/ui/ui_exports/ui_export_types/reduce/lib/index.js new file mode 100644 index 00000000000000..79e42849a3f3e0 --- /dev/null +++ b/src/ui/ui_exports/ui_export_types/reduce/lib/index.js @@ -0,0 +1,3 @@ +export { flatConcat } from './flat_concat'; +export { mergeWith } from './merge_with'; +export { createTypeReducer } from './create_type_reducer'; diff --git a/src/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js b/src/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js new file mode 100644 index 00000000000000..edeab703f70c58 --- /dev/null +++ b/src/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js @@ -0,0 +1,22 @@ +const uniqueConcat = (arrayA, arrayB) => arrayB.reduce((acc, key) => ( + acc.includes(key) + ? acc + : acc.concat(key) +), arrayA); + +/** + * Assign the keys from both objA and objB to target after passing the + * current and new value through merge as `(target[key], source[key])` + * @param {Object} objA + * @param {Object} objB + * @param {Function} merge + * @return {Object} target + */ +export function mergeWith(objA, objB, merge) { + const target = {}; + const keys = uniqueConcat(Object.keys(objA), Object.keys(objB)); + for (const key of keys) { + target[key] = merge(objA[key], objB[key]); + } + return target; +} diff --git a/src/ui/ui_exports/ui_export_types/reduce/merge.js b/src/ui/ui_exports/ui_export_types/reduce/merge.js deleted file mode 100644 index da7d3226acfa44..00000000000000 --- a/src/ui/ui_exports/ui_export_types/reduce/merge.js +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeType } from './merge_type'; - -export const merge = mergeType((a, b) => ({ - ...a, - ...b -})); diff --git a/src/ui/ui_exports/ui_export_types/reduce/merge_at_type.js b/src/ui/ui_exports/ui_export_types/reduce/merge_at_type.js new file mode 100644 index 00000000000000..e2b79837c25f32 --- /dev/null +++ b/src/ui/ui_exports/ui_export_types/reduce/merge_at_type.js @@ -0,0 +1,6 @@ +import { createTypeReducer } from './lib'; + +export const mergeAtType = createTypeReducer((a, b) => ({ + ...a, + ...b +})); diff --git a/src/ui/ui_exports/ui_export_types/reduce/merge_type.js b/src/ui/ui_exports/ui_export_types/reduce/merge_type.js deleted file mode 100644 index 87b52a44b2cfb6..00000000000000 --- a/src/ui/ui_exports/ui_export_types/reduce/merge_type.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Creates a reducer that merges the values within `acc[type]` by calling - * `merge` with `(acc[type], spec, type, pluginSpec)` - * @param {Function} merge receives `(acc[type], spec, type, pluginSpec)` - * @return {Function} - */ -export const mergeType = (merge) => (acc, spec, type, pluginSpec) => ({ - ...acc, - [type]: merge(acc[type], spec, type, pluginSpec) -}); diff --git a/src/ui/ui_exports/ui_export_types/reduce/unique_assign.js b/src/ui/ui_exports/ui_export_types/reduce/unique_assign.js deleted file mode 100644 index 11313fe1f8ecc0..00000000000000 --- a/src/ui/ui_exports/ui_export_types/reduce/unique_assign.js +++ /dev/null @@ -1,12 +0,0 @@ -export function uniqueAssign(acc, spec, type, pluginSpec) { - return Object.keys(spec).reduce((acc, key) => { - if (acc.hasOwnProperty(key)) { - throw new Error(`${pluginSpec.getId()} defines a duplicate ${type} for key ${key}`); - } - - return { - ...acc, - [key]: spec[key] - }; - }, acc); -} diff --git a/src/ui/ui_exports/ui_export_types/saved_object_mappings.js b/src/ui/ui_exports/ui_export_types/saved_object_mappings.js index ebbc9ef18b100d..118e08824fd959 100644 --- a/src/ui/ui_exports/ui_export_types/saved_object_mappings.js +++ b/src/ui/ui_exports/ui_export_types/saved_object_mappings.js @@ -1,4 +1,4 @@ -import { concat } from './reduce'; +import { flatConcatAtType } from './reduce'; import { alias, mapSpec, wrap } from './modify_reduce'; // mapping types @@ -8,5 +8,5 @@ export const mappings = wrap( pluginId: pluginSpec.getId(), properties: spec })), - concat + flatConcatAtType ); diff --git a/src/ui/ui_exports/ui_export_types/ui_app_extensions.js b/src/ui/ui_exports/ui_export_types/ui_app_extensions.js index 8e493df3103f5e..edfa103e264e84 100644 --- a/src/ui/ui_exports/ui_export_types/ui_app_extensions.js +++ b/src/ui/ui_exports/ui_export_types/ui_app_extensions.js @@ -1,4 +1,4 @@ -import { concatValues } from './reduce'; +import { flatConcatValuesAtType } from './reduce'; import { mapSpec, alias, wrap } from './modify_reduce'; /** @@ -9,7 +9,7 @@ import { mapSpec, alias, wrap } from './modify_reduce'; const appExtension = wrap( mapSpec((spec, type) => ({ [type]: spec })), alias('appExtensions'), - concatValues + flatConcatValuesAtType ); // plain extension groups produce lists of modules that will be required by the entry @@ -36,4 +36,4 @@ export const visTypeEnhancers = wrap(alias('visTypes'), appExtension); // adhoc extension groups can define new extension groups on the fly // so that plugins could concat their own -export const aliases = concatValues; +export const aliases = flatConcatValuesAtType; diff --git a/src/ui/ui_exports/ui_export_types/ui_apps.js b/src/ui/ui_exports/ui_export_types/ui_apps.js index c11977a03bd161..5e801c675f601b 100644 --- a/src/ui/ui_exports/ui_export_types/ui_apps.js +++ b/src/ui/ui_exports/ui_export_types/ui_apps.js @@ -1,6 +1,6 @@ import { noop, uniq } from 'lodash'; -import { concat } from './reduce'; +import { flatConcatAtType } from './reduce'; import { alias, mapSpec, wrap } from './modify_reduce'; function applySpecDefaults(spec, type, pluginSpec) { @@ -43,5 +43,5 @@ function applySpecDefaults(spec, type, pluginSpec) { }; } -export const apps = wrap(alias('uiAppSpecs'), mapSpec(applySpecDefaults), concat); -export const app = wrap(alias('uiAppSpecs'), mapSpec(applySpecDefaults), concat); +export const apps = wrap(alias('uiAppSpecs'), mapSpec(applySpecDefaults), flatConcatAtType); +export const app = wrap(alias('uiAppSpecs'), mapSpec(applySpecDefaults), flatConcatAtType); diff --git a/src/ui/ui_exports/ui_export_types/ui_i18n.js b/src/ui/ui_exports/ui_export_types/ui_i18n.js index 9e589583394c8e..c734562f7737d1 100644 --- a/src/ui/ui_exports/ui_export_types/ui_i18n.js +++ b/src/ui/ui_exports/ui_export_types/ui_i18n.js @@ -1,5 +1,5 @@ -import { concat } from './reduce'; +import { flatConcatAtType } from './reduce'; import { wrap, alias } from './modify_reduce'; // paths to translation files -export const translations = wrap(alias('translationPaths'), concat); +export const translations = wrap(alias('translationPaths'), flatConcatAtType); diff --git a/src/ui/ui_exports/ui_export_types/ui_nav_links.js b/src/ui/ui_exports/ui_export_types/ui_nav_links.js index 9a9a3841a988e7..e17a1970c562e5 100644 --- a/src/ui/ui_exports/ui_export_types/ui_nav_links.js +++ b/src/ui/ui_exports/ui_export_types/ui_nav_links.js @@ -1,5 +1,5 @@ -import { concat } from './reduce'; +import { flatConcatAtType } from './reduce'; import { wrap, alias } from './modify_reduce'; -export const links = wrap(alias('navLinkSpecs'), concat); -export const link = wrap(alias('navLinkSpecs'), concat); +export const links = wrap(alias('navLinkSpecs'), flatConcatAtType); +export const link = wrap(alias('navLinkSpecs'), flatConcatAtType); diff --git a/src/ui/ui_exports/ui_export_types/ui_settings.js b/src/ui/ui_exports/ui_export_types/ui_settings.js index b522e89f9e1fa9..024c53682a7a7a 100644 --- a/src/ui/ui_exports/ui_export_types/ui_settings.js +++ b/src/ui/ui_exports/ui_export_types/ui_settings.js @@ -1,4 +1,4 @@ -import { merge } from './reduce'; +import { mergeAtType } from './reduce'; import { wrap, uniqueKeys } from './modify_reduce'; -export const uiSettingDefaults = wrap(uniqueKeys(), merge); +export const uiSettingDefaults = wrap(uniqueKeys(), mergeAtType); diff --git a/src/ui/ui_exports/ui_export_types/unknown.js b/src/ui/ui_exports/ui_export_types/unknown.js index 778d89c16f28d3..27b3430428d2a5 100644 --- a/src/ui/ui_exports/ui_export_types/unknown.js +++ b/src/ui/ui_exports/ui_export_types/unknown.js @@ -1,4 +1,4 @@ -import { concat } from './reduce'; +import { flatConcatAtType } from './reduce'; import { wrap, alias, debug } from './modify_reduce'; -export const unknown = wrap(debug, alias('unknown'), concat); +export const unknown = wrap(debug, alias('unknown'), flatConcatAtType); diff --git a/src/ui/ui_exports/ui_export_types/webpack_customizations.js b/src/ui/ui_exports/ui_export_types/webpack_customizations.js index d2096ecc48224c..4b5966efa8ebc1 100644 --- a/src/ui/ui_exports/ui_export_types/webpack_customizations.js +++ b/src/ui/ui_exports/ui_export_types/webpack_customizations.js @@ -2,11 +2,11 @@ import { isAbsolute } from 'path'; import { escapeRegExp } from 'lodash'; -import { concat, merge } from './reduce'; +import { flatConcatAtType, mergeAtType } from './reduce'; import { alias, wrap, uniqueKeys, mapSpec } from './modify_reduce'; -export const __globalImportAliases__ = wrap(alias('webpackAliases'), uniqueKeys('__globalImportAliases__'), merge); -export const __bundleProvider__ = wrap(alias('uiBundleProviders'), concat); +export const __globalImportAliases__ = wrap(alias('webpackAliases'), uniqueKeys('__globalImportAliases__'), mergeAtType); +export const __bundleProvider__ = wrap(alias('uiBundleProviders'), flatConcatAtType); export const noParse = wrap( alias('webpackNoParseRules'), mapSpec(rule => { @@ -20,5 +20,5 @@ export const noParse = wrap( throw new Error('Expected noParse rule to be a string or regexp'); }), - concat + flatConcatAtType ); From c9eff804121a215f42d9f9263ddce3e48ab89a3e Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 20:13:18 -0700 Subject: [PATCH 41/67] [ui/uiExports] add README --- src/ui/ui_exports/README.md | 96 +++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/ui/ui_exports/README.md diff --git a/src/ui/ui_exports/README.md b/src/ui/ui_exports/README.md new file mode 100644 index 00000000000000..a229fa7dd45d97 --- /dev/null +++ b/src/ui/ui_exports/README.md @@ -0,0 +1,96 @@ +# UI Exports + +When defining a Plugin, the `uiExports` key can be used to define a map of export types to values that will be used to configure the UI system. A common use for `uiExports` is `uiExports.app`, which defines the configuration of a [`UiApp`](UiApp) and teaches the UI System how to render, bundle and tell the user about an application. + + +## `collectUiExports(pluginSpecs): { [type: string]: any }` + +This function produces the object commonly found at `kbnServer.uiExports`. This object is created by calling `collectPluginExports()` with a standard set of export type reducers and defaults for the UI System. + +### export type reducers + +The [`ui_export_types` module](UiExportTypes) defines the reducer used for each uiExports key (or `type`). The name of every export in [./ui_export_types/index.js](UiExportTypes) is a key that plugins can define in their `uiExports` specification and the value of those exports are reducers that `collectPluginExports()` will call to produce the merged result of all export specs. + +### example - UiApps + +Plugin authors can define a new UiApp in their plugin specification like so: + +```js +// a single app export +export default function (kibana) { + return new kibana.Plugin({ + //... + uiExports: { + app: { + // uiApp spec options go here + } + } + }) +} + +// apps can also export multiple apps +export default function (kibana) { + return new kibana.Plugin({ + //... + uiExports: { + apps: [ + { /* uiApp spec options */ }, + { /* second uiApp spec options */ }, + ] + } + }) +} +``` + +To handle this export type, the [ui_export_types](UiExportTypes) module exports two reducers, one named `app` and the other `apps`. + +```js +export const app = ... +export const apps = ... +``` + +These reducers are defined in [`ui_export_types/ui_apps`](UiAppExportType) and have the exact same definition: + +```js +// `wrap()` produces a reducer by wrapping a base reducer with modifiers. +// All but the last argument are modifiers that take a reducer and return +// an alternate reducer to use in it's place. +// +// Most wrappers call their target reducer with slightly different +// arguments. This allows composing standard reducer modifications for +// reuse, consistency, and easy reference (once you get the hang of it). +wrap( + // calls the next reducer with the `type` set to `uiAppSpecs`, ignoring + // the key the plugin author used to define this spec ("app" or "apps" + // in this example) + alias('uiAppSpecs'), + + // calls the next reducer with a modified version which merges some + // default values from the `PluginSpec` because we want uiAppSpecs + // to be useful without depending on the `PluginSpec` + mapSpec(applySpecDefaults), + + // writes this spec to `acc[type]` (`acc.uiAppSpecs` in this example since + // type was set by `alias()` above). It does this by using `flatConcat`, + // which concatenates two values into a single array. If either item + // is an array their items are added to the result individually. If either + // item is undefined it is ignored. + // + // NOTE: since flatConcatAtType is last it isn't a wrapper, it's + // just a normal reducers + flatConcatAtType +) +``` + +The reducer probably looks super foreign right now, but hopefully soon you'll be able to look back at these reducers and see that `app` and `apps` export specs are written to `kbnServer.uiExports.uiAppSpecs` with defaults applied, in an array. + +### defaults + +The [./ui_export_defaults](UiExportDefaults) module defines the default shape of the uiExports object produced by `collectUiExports()`. The defaults generally describe the `uiExports` from the UI System itself, like default visTypes and such. + +[UiApp]: ../ui_apps/ui_app.js "UiApp class definition" +[UiExportTypes]: ./ui_export_defaults.js "uiExport defaults definition" +[UiExportTypes]: ./ui_export_types/index.js "Index of default ui_export_types module" +[UiAppExportType]: ./ui_export_types/ui_apps.js "UiApp extension type definition" +[PluginSpec]: ../../plugin_discovery/plugin_spec/plugin_spec.js "PluginSpec class definition" +[PluginDiscovery]: '../../plugin_discovery' "plugin_discovery module" \ No newline at end of file From 0606846cb2802a59202bfb061c8b931f97de96d2 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 20:23:52 -0700 Subject: [PATCH 42/67] fix links --- src/plugin_discovery/README.md | 24 ++++++++++++------------ src/ui/ui_exports/README.md | 10 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/plugin_discovery/README.md b/src/plugin_discovery/README.md index c764fed81b5937..6aaf1868ffa025 100644 --- a/src/plugin_discovery/README.md +++ b/src/plugin_discovery/README.md @@ -5,11 +5,11 @@ The plugin discovery module defines the core plugin loading logic used by the Ki ## `findPluginSpecs(settings, [config])` -Finds [`PluginSpec`](PluginSpec) objects +Finds [`PluginSpec`][PluginSpec] objects ### params - - `settings`: the same settings object accepted by [`KbnServer`](KbnServer) - - `[config]`: Optional - a [`Config`](Config) service. Using this param causes `findPluginSpecs()` to modify `config`'s schema to support the configuration for each discovered [`PluginSpec`](PluginSpec). If you can, please use the [`Config`](Config) service produced by `extendedConfig$` rather than passing in an existing service so that `findPluginSpecs()` is side-effect free. + - `settings`: the same settings object accepted by [`KbnServer`][KbnServer] + - `[config]`: Optional - a [`Config`][Config] service. Using this param causes `findPluginSpecs()` to modify `config`'s schema to support the configuration for each discovered [`PluginSpec`][PluginSpec]. If you can, please use the [`Config`][Config] service produced by `extendedConfig$` rather than passing in an existing service so that `findPluginSpecs()` is side-effect free. ### return value @@ -17,14 +17,14 @@ Finds [`PluginSpec`](PluginSpec) objects If you *never* subscribe to any of the Observables then plugin discovery won't actually run. - - `pack$`: emits every [`PluginPack`](PluginPack) found - - `invalidDirectoryError$: Observable`: emits [`InvalidDirectoryError`](Errors)s caused by `settings.plugins.scanDirs` values that don't point to actual directories. `findPluginSpecs()` will not abort when this error is encountered. - - `invalidPackError$: Observable`: emits [`InvalidPackError`](Errors)s caused by children of `settings.plugins.scanDirs` or `settings.plugins.paths` values which don't meet the requirements of a [`PluginPack`](PluginPack) (probably missing a `package.json`). `findPluginSpecs()` will not abort when this error is encountered. - - `deprecation$: Observable`: emits deprecation warnings that are produces when reading each [`PluginPack`](PluginPack)'s configuration - - `extendedConfig$: Observable`: emits the [`Config`](Config) service that was passed to `findPluginSpecs()` (or created internally if none was passed) after it has been extended with the configuration from each plugin - - `spec$: Observable`: emits every *enabled* [`PluginSpec`](PluginSpec) defined by the discovered [`PluginPack`](PluginPack)s - - `disabledSpecs$: Observable`: emits every *disabled* [`PluginSpec`](PluginSpec) defined by the discovered [`PluginPack`](PluginPack)s - - `invalidVersionSpec$: Observable`: emits every [`PluginSpec`](PluginSpec) who's required kibana version does not match the version exposed by `config.get('pkg.version')` + - `pack$`: emits every [`PluginPack`][PluginPack] found + - `invalidDirectoryError$: Observable`: emits [`InvalidDirectoryError`][Errors]s caused by `settings.plugins.scanDirs` values that don't point to actual directories. `findPluginSpecs()` will not abort when this error is encountered. + - `invalidPackError$: Observable`: emits [`InvalidPackError`][Errors]s caused by children of `settings.plugins.scanDirs` or `settings.plugins.paths` values which don't meet the requirements of a [`PluginPack`][PluginPack] (probably missing a `package.json`). `findPluginSpecs()` will not abort when this error is encountered. + - `deprecation$: Observable`: emits deprecation warnings that are produces when reading each [`PluginPack`][PluginPack]'s configuration + - `extendedConfig$: Observable`: emits the [`Config`][Config] service that was passed to `findPluginSpecs()` (or created internally if none was passed) after it has been extended with the configuration from each plugin + - `spec$: Observable`: emits every *enabled* [`PluginSpec`][PluginSpec] defined by the discovered [`PluginPack`][PluginPack]s + - `disabledSpecs$: Observable`: emits every *disabled* [`PluginSpec`][PluginSpec] defined by the discovered [`PluginPack`][PluginPack]s + - `invalidVersionSpec$: Observable`: emits every [`PluginSpec`][PluginSpec] who's required kibana version does not match the version exposed by `config.get('pkg.version')` ### example @@ -80,7 +80,7 @@ Observable.merge( ## `reduceExportSpecs(pluginSpecs, reducers, [defaults={}])` -Iterates through every export specification provided by all [`PluginSpec`](PluginSpec)s. If an exported specification value is an array each item in the array will be passed to the reducer individually. If the exported specification is `undefined` it will be ignored. The reducer is called with the signature: +Iterates through every export specification provided by all [`PluginSpec`][PluginSpec]s. If an exported specification value is an array each item in the array will be passed to the reducer individually. If the exported specification is `undefined` it will be ignored. The reducer is called with the signature: ```js reducer( diff --git a/src/ui/ui_exports/README.md b/src/ui/ui_exports/README.md index a229fa7dd45d97..4fb3f3c6bc5513 100644 --- a/src/ui/ui_exports/README.md +++ b/src/ui/ui_exports/README.md @@ -1,6 +1,6 @@ # UI Exports -When defining a Plugin, the `uiExports` key can be used to define a map of export types to values that will be used to configure the UI system. A common use for `uiExports` is `uiExports.app`, which defines the configuration of a [`UiApp`](UiApp) and teaches the UI System how to render, bundle and tell the user about an application. +When defining a Plugin, the `uiExports` key can be used to define a map of export types to values that will be used to configure the UI system. A common use for `uiExports` is `uiExports.app`, which defines the configuration of a [`UiApp`][UiApp] and teaches the UI System how to render, bundle and tell the user about an application. ## `collectUiExports(pluginSpecs): { [type: string]: any }` @@ -9,7 +9,7 @@ This function produces the object commonly found at `kbnServer.uiExports`. This ### export type reducers -The [`ui_export_types` module](UiExportTypes) defines the reducer used for each uiExports key (or `type`). The name of every export in [./ui_export_types/index.js](UiExportTypes) is a key that plugins can define in their `uiExports` specification and the value of those exports are reducers that `collectPluginExports()` will call to produce the merged result of all export specs. +The [`ui_export_types` module][UiExportTypes] defines the reducer used for each uiExports key (or `type`). The name of every export in [./ui_export_types/index.js][UiExportTypes] is a key that plugins can define in their `uiExports` specification and the value of those exports are reducers that `collectPluginExports()` will call to produce the merged result of all export specs. ### example - UiApps @@ -42,14 +42,14 @@ export default function (kibana) { } ``` -To handle this export type, the [ui_export_types](UiExportTypes) module exports two reducers, one named `app` and the other `apps`. +To handle this export type, the [ui_export_types][UiExportTypes] module exports two reducers, one named `app` and the other `apps`. ```js export const app = ... export const apps = ... ``` -These reducers are defined in [`ui_export_types/ui_apps`](UiAppExportType) and have the exact same definition: +These reducers are defined in [`ui_export_types/ui_apps`][UiAppExportType] and have the exact same definition: ```js // `wrap()` produces a reducer by wrapping a base reducer with modifiers. @@ -86,7 +86,7 @@ The reducer probably looks super foreign right now, but hopefully soon you'll be ### defaults -The [./ui_export_defaults](UiExportDefaults) module defines the default shape of the uiExports object produced by `collectUiExports()`. The defaults generally describe the `uiExports` from the UI System itself, like default visTypes and such. +The [./ui_export_defaults][UiExportDefaults] module defines the default shape of the uiExports object produced by `collectUiExports()`. The defaults generally describe the `uiExports` from the UI System itself, like default visTypes and such. [UiApp]: ../ui_apps/ui_app.js "UiApp class definition" [UiExportTypes]: ./ui_export_defaults.js "uiExport defaults definition" From 1898ae401d3aa5edb5f11e6766c1cc893ff0a3ac Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 20:33:53 -0700 Subject: [PATCH 43/67] [pluginDiscovery/readme] expand examples --- src/plugin_discovery/README.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/plugin_discovery/README.md b/src/plugin_discovery/README.md index 6aaf1868ffa025..84d91d23a74570 100644 --- a/src/plugin_discovery/README.md +++ b/src/plugin_discovery/README.md @@ -28,10 +28,37 @@ If you *never* subscribe to any of the Observables then plugin discovery won't a ### example +Just get the plugin specs, only fail if there is an uncaught error of some sort: ```js -import { readYamlConfig } from 'src/cli/serve/read_yaml_config' -const settings = readYamlConfig('config/kibana.yml') +const { pack$ } = findPluginSpecs(settings); +const packs = await pack$.toArray().toPromise() +``` + +Just log the deprecation messages: +```js +const { deprecation$ } = findPluginSpecs(settings); +for (const warning of await deprecation$.toArray().toPromise()) { + console.log('DEPRECATION:', warning) +} +``` +Get the packs but fail if any packs are invalid: +```js +const { pack$, invalidDirectoryError$ } = findPluginSpecs(settings); +const packs = await Observable.merge( + pack$.toArray(), + + // if we ever get an InvalidDirectoryError, throw it + // into the stream so that all streams are unsubscribed, + // the discovery process is aborted, and the promise rejects + invalidDirectoryError$.map(error => { + throw error + }), +).toPromise() +``` + +Handle everything +```js const { pack$, invalidDirectoryError$, From 89efa7b7f46f00a8c5d49864dadeb197dfaeb352 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 20:37:42 -0700 Subject: [PATCH 44/67] [pluginDiscovery/readme] clean up reduceExportSpecs() doc --- src/plugin_discovery/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugin_discovery/README.md b/src/plugin_discovery/README.md index 84d91d23a74570..90d166b3ea9440 100644 --- a/src/plugin_discovery/README.md +++ b/src/plugin_discovery/README.md @@ -107,18 +107,18 @@ Observable.merge( ## `reduceExportSpecs(pluginSpecs, reducers, [defaults={}])` -Iterates through every export specification provided by all [`PluginSpec`][PluginSpec]s. If an exported specification value is an array each item in the array will be passed to the reducer individually. If the exported specification is `undefined` it will be ignored. The reducer is called with the signature: +Reduces every value exported by the [`PluginSpec`][PluginSpec]s to produce a single value. If an exported value is an array each item in the array will be reduced individually. If the exported value is `undefined` it will be ignored. The reducer is called with the signature: ```js reducer( // the result of the previous reducer call, or `defaults` acc: any, - // the value exported value, found at `uiExports[type]` in the - // PluginSpec config + // the exported value, found at `uiExports[type]` or `uiExports[type][i]` + // in the PluginSpec config. spec: any, // the key in `uiExports` where this export was found type: string, - // the PluginSpec which provided this export + // the PluginSpec which exported this spec pluginSpec: PluginSpec ) ``` From 6d14b2281cf04b1605272d604ac3582cf7cafc68 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 20:45:41 -0700 Subject: [PATCH 45/67] [ui/uiExports/readme] cleanup example --- src/ui/ui_exports/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ui/ui_exports/README.md b/src/ui/ui_exports/README.md index 4fb3f3c6bc5513..05a142e598f9d2 100644 --- a/src/ui/ui_exports/README.md +++ b/src/ui/ui_exports/README.md @@ -65,24 +65,24 @@ wrap( // in this example) alias('uiAppSpecs'), - // calls the next reducer with a modified version which merges some - // default values from the `PluginSpec` because we want uiAppSpecs - // to be useful without depending on the `PluginSpec` + // calls the next reducer with the `spec` set to the result of calling + // `applySpecDefaults(spec, type, pluginSpec)` which merges some defaults + // from the `PluginSpec` because we want uiAppSpecs to be useful individually mapSpec(applySpecDefaults), // writes this spec to `acc[type]` (`acc.uiAppSpecs` in this example since - // type was set by `alias()` above). It does this by using `flatConcat`, - // which concatenates two values into a single array. If either item - // is an array their items are added to the result individually. If either - // item is undefined it is ignored. + // the type was set to `uiAppSpecs` by `alias()`). It does this by concatenating + // the current value and the spec into an array. If either item is already + // an array its items are added to the result individually. If either item + // is undefined it is ignored. // // NOTE: since flatConcatAtType is last it isn't a wrapper, it's - // just a normal reducers + // just a normal reducer flatConcatAtType ) ``` -The reducer probably looks super foreign right now, but hopefully soon you'll be able to look back at these reducers and see that `app` and `apps` export specs are written to `kbnServer.uiExports.uiAppSpecs` with defaults applied, in an array. +This reducer format was chosen so that it will be easier to look back at these reducers and see that `app` and `apps` export specs are written to `kbnServer.uiExports.uiAppSpecs`, with defaults applied, in an array. ### defaults From b8a95b7239af2cca478a01cbc3c29583b6f5c3e1 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 21:00:14 -0700 Subject: [PATCH 46/67] [pluginDiscovery] remove needless use of lodash --- src/plugin_discovery/plugin_config/schema.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plugin_discovery/plugin_config/schema.js b/src/plugin_discovery/plugin_config/schema.js index f6e1fe8e643b48..c2cfbeacd58370 100644 --- a/src/plugin_discovery/plugin_config/schema.js +++ b/src/plugin_discovery/plugin_config/schema.js @@ -1,5 +1,4 @@ import Joi from 'joi'; -import { noop } from 'lodash'; const STUB_CONFIG_SCHEMA = Joi.object().keys({ enabled: Joi.valid(false) @@ -16,8 +15,8 @@ const DEFAULT_CONFIG_SCHEMA = Joi.object().keys({ * @return {Promise} */ export async function getSchema(spec) { - const provider = spec.getConfigSchemaProvider() || noop; - return (await provider(Joi)) || DEFAULT_CONFIG_SCHEMA; + const provider = spec.getConfigSchemaProvider(); + return (provider && await provider(Joi)) || DEFAULT_CONFIG_SCHEMA; } export function getStubSchema() { From 4cda3659fe20ca0468c03e3728e7e9a91d3d6b76 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 21:01:38 -0700 Subject: [PATCH 47/67] [pluginDiscovery/waitForComplete] use better name --- src/plugin_discovery/find_plugin_specs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugin_discovery/find_plugin_specs.js b/src/plugin_discovery/find_plugin_specs.js index 7edae71c2c018b..8eb77c7d359690 100644 --- a/src/plugin_discovery/find_plugin_specs.js +++ b/src/plugin_discovery/find_plugin_specs.js @@ -23,7 +23,7 @@ function defaultConfig(settings) { ); } -function waitForComplete(observable) { +function bufferAllResults(observable) { return observable // buffer all results into a single array .toArray() @@ -68,7 +68,7 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { }; }) // extend the config with all plugins before determining enabled status - .let(waitForComplete) + .let(bufferAllResults) .map(({ spec, deprecations }) => { const isRightVersion = spec.isVersionCompatible(config.get('pkg.version')); const enabled = isRightVersion && spec.isEnabled(config); @@ -81,7 +81,7 @@ export function findPluginSpecs(settings, config = defaultConfig(settings)) { }; }) // determine which plugins are disabled before actually removing things from the config - .let(waitForComplete) + .let(bufferAllResults) .do(result => { for (const spec of result.disabledSpecs) { disableConfigExtension(spec, config); From fc73f4bf397d1a1b2d0470633560fe522e82850c Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 21:15:45 -0700 Subject: [PATCH 48/67] [pluginDiscovery/findPluginSpecs] use fixtures rather than core_plugins --- .../__tests__/find_plugin_specs.js | 48 ++++++++++--------- .../__tests__/fixtures/plugins/bar/index.js | 10 ++++ .../fixtures/plugins/bar/package.json | 4 ++ .../__tests__/fixtures/plugins/foo/index.js | 5 ++ .../fixtures/plugins/foo/package.json | 4 ++ 5 files changed, 48 insertions(+), 23 deletions(-) create mode 100644 src/plugin_discovery/__tests__/fixtures/plugins/bar/index.js create mode 100644 src/plugin_discovery/__tests__/fixtures/plugins/bar/package.json create mode 100644 src/plugin_discovery/__tests__/fixtures/plugins/foo/index.js create mode 100644 src/plugin_discovery/__tests__/fixtures/plugins/foo/package.json diff --git a/src/plugin_discovery/__tests__/find_plugin_specs.js b/src/plugin_discovery/__tests__/find_plugin_specs.js index be3485ee0ac0d5..b0361b8d0e877c 100644 --- a/src/plugin_discovery/__tests__/find_plugin_specs.js +++ b/src/plugin_discovery/__tests__/find_plugin_specs.js @@ -1,11 +1,10 @@ import { resolve } from 'path'; -import { readdirSync } from 'fs'; import expect from 'expect.js'; import { findPluginSpecs } from '../find_plugin_specs'; import { PluginSpec } from '../plugin_spec'; -const CORE_PLUGINS = resolve(__dirname, '../../core_plugins'); +const PLUGIN_FIXTURES = resolve(__dirname, 'fixtures/plugins'); describe('plugin discovery', () => { describe('findPluginSpecs()', function () { @@ -15,17 +14,19 @@ describe('plugin discovery', () => { const { spec$ } = findPluginSpecs({ plugins: { paths: [ - resolve(CORE_PLUGINS, 'console'), - resolve(CORE_PLUGINS, 'elasticsearch'), + resolve(PLUGIN_FIXTURES, 'foo'), + resolve(PLUGIN_FIXTURES, 'bar'), ] } }); const specs = await spec$.toArray().toPromise(); - expect(specs).to.have.length(2); - expect(specs[0]).to.be.a(PluginSpec); - expect(specs[0].getId()).to.be('console'); - expect(specs[1].getId()).to.be('elasticsearch'); + expect(specs).to.have.length(3); + specs.forEach(spec => { + expect(spec).to.be.a(PluginSpec); + }); + expect(specs.map(s => s.getId()).sort()) + .to.eql(['bar:one', 'bar:two', 'foo']); }); it('finds all specs in scanDirs', async () => { @@ -34,39 +35,40 @@ describe('plugin discovery', () => { env: 'development', plugins: { - scanDirs: [CORE_PLUGINS] + scanDirs: [PLUGIN_FIXTURES] } }); - const expected = readdirSync(CORE_PLUGINS) - .filter(name => !name.startsWith('.')) - .sort((a, b) => a.localeCompare(b)); - const specs = await spec$.toArray().toPromise(); - const specIds = specs - .map(spec => spec.getId()) - .sort((a, b) => a.localeCompare(b)); - - expect(specIds).to.eql(expected); + expect(specs).to.have.length(3); + specs.forEach(spec => { + expect(spec).to.be.a(PluginSpec); + }); + expect(specs.map(s => s.getId()).sort()) + .to.eql(['bar:one', 'bar:two', 'foo']); }); it('does not find disabled plugins', async () => { const { spec$ } = findPluginSpecs({ - elasticsearch: { + 'bar:one': { enabled: false }, plugins: { paths: [ - resolve(CORE_PLUGINS, 'elasticsearch'), - resolve(CORE_PLUGINS, 'kibana') + resolve(PLUGIN_FIXTURES, 'foo'), + resolve(PLUGIN_FIXTURES, 'bar') ] } }); const specs = await spec$.toArray().toPromise(); - expect(specs).to.have.length(1); - expect(specs[0].getId()).to.be('kibana'); + expect(specs).to.have.length(2); + specs.forEach(spec => { + expect(spec).to.be.a(PluginSpec); + }); + expect(specs.map(s => s.getId()).sort()) + .to.eql(['bar:two', 'foo']); }); }); }); diff --git a/src/plugin_discovery/__tests__/fixtures/plugins/bar/index.js b/src/plugin_discovery/__tests__/fixtures/plugins/bar/index.js new file mode 100644 index 00000000000000..32aea33e36c334 --- /dev/null +++ b/src/plugin_discovery/__tests__/fixtures/plugins/bar/index.js @@ -0,0 +1,10 @@ +export default function (kibana) { + return [ + new kibana.Plugin({ + id: 'bar:one', + }), + new kibana.Plugin({ + id: 'bar:two', + }), + ]; +} diff --git a/src/plugin_discovery/__tests__/fixtures/plugins/bar/package.json b/src/plugin_discovery/__tests__/fixtures/plugins/bar/package.json new file mode 100644 index 00000000000000..e43c2f0bc984cf --- /dev/null +++ b/src/plugin_discovery/__tests__/fixtures/plugins/bar/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "version": "kibana" +} diff --git a/src/plugin_discovery/__tests__/fixtures/plugins/foo/index.js b/src/plugin_discovery/__tests__/fixtures/plugins/foo/index.js new file mode 100644 index 00000000000000..cbb05e0deed528 --- /dev/null +++ b/src/plugin_discovery/__tests__/fixtures/plugins/foo/index.js @@ -0,0 +1,5 @@ +module.exports = function (kibana) { + return new kibana.Plugin({ + id: 'foo', + }); +}; diff --git a/src/plugin_discovery/__tests__/fixtures/plugins/foo/package.json b/src/plugin_discovery/__tests__/fixtures/plugins/foo/package.json new file mode 100644 index 00000000000000..e43c2f0bc984cf --- /dev/null +++ b/src/plugin_discovery/__tests__/fixtures/plugins/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "version": "kibana" +} From 70666a2ee7c6fe426416b1958ef21a1fb26e164a Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 22:13:12 -0700 Subject: [PATCH 49/67] [pluginDiscovery/stubSchema] use deafult: false --- src/plugin_discovery/plugin_config/schema.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugin_discovery/plugin_config/schema.js b/src/plugin_discovery/plugin_config/schema.js index c2cfbeacd58370..5bddb6bd39ecba 100644 --- a/src/plugin_discovery/plugin_config/schema.js +++ b/src/plugin_discovery/plugin_config/schema.js @@ -1,8 +1,8 @@ import Joi from 'joi'; const STUB_CONFIG_SCHEMA = Joi.object().keys({ - enabled: Joi.valid(false) -}); + enabled: Joi.valid(false).default(false) +}).default(); const DEFAULT_CONFIG_SCHEMA = Joi.object().keys({ enabled: Joi.boolean().default(true) From b5079a5d99fb10414555598ae79e287bcd321e13 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 22:22:28 -0700 Subject: [PATCH 50/67] [plguinDiscovery/pluginConfig] add tests --- .../__tests__/extend_config_service.js | 177 ++++++++++++++++++ .../plugin_config/__tests__/schema.js | 72 +++++++ .../plugin_config/__tests__/settings.js | 64 +++++++ 3 files changed, 313 insertions(+) create mode 100644 src/plugin_discovery/plugin_config/__tests__/extend_config_service.js create mode 100644 src/plugin_discovery/plugin_config/__tests__/schema.js create mode 100644 src/plugin_discovery/plugin_config/__tests__/settings.js diff --git a/src/plugin_discovery/plugin_config/__tests__/extend_config_service.js b/src/plugin_discovery/plugin_config/__tests__/extend_config_service.js new file mode 100644 index 00000000000000..ca1f9adbd3104e --- /dev/null +++ b/src/plugin_discovery/plugin_config/__tests__/extend_config_service.js @@ -0,0 +1,177 @@ +import sinon from 'sinon'; +import expect from 'expect.js'; + +import { Config } from '../../../server/config'; +import { PluginPack } from '../../plugin_pack'; +import { extendConfigService, disableConfigExtension } from '../extend_config_service'; +import * as SchemaNS from '../schema'; +import * as SettingsNS from '../settings'; + +describe('plugin discovery/extend config service', () => { + const sandbox = sinon.sandbox.create(); + afterEach(() => sandbox.restore()); + + const pluginSpec = new PluginPack({ + path: '/dev/null', + pkg: { + name: 'test', + version: 'kibana', + }, + provider: ({ Plugin }) => new Plugin({ + configPrefix: 'foo.bar.baz', + + config: Joi => Joi.object({ + enabled: Joi.boolean().default(true), + test: Joi.string().default('bonk'), + }).default(), + + deprecations({ rename }) { + return [ + rename('oldTest', 'test'), + ]; + }, + }), + }) + .getPluginSpecs() + .pop(); + + describe('extendConfigService()', () => { + it('calls getSettings, getSchema, and Config.extendSchema() correctly', async () => { + const rootSettings = { + foo: { + bar: { + enabled: false + } + } + }; + const schema = { + validate: () => {} + }; + const configPrefix = 'foo.bar'; + const config = { + extendSchema: sandbox.stub() + }; + const pluginSpec = { + getConfigPrefix: sandbox.stub() + .returns(configPrefix) + }; + + const logDeprecation = sandbox.stub(); + + const getSettings = sandbox.stub(SettingsNS, 'getSettings') + .returns(rootSettings.foo.bar); + + const getSchema = sandbox.stub(SchemaNS, 'getSchema') + .returns(schema); + + await extendConfigService(pluginSpec, config, rootSettings, logDeprecation); + + sinon.assert.calledOnce(getSettings); + sinon.assert.calledWithExactly(getSettings, pluginSpec, rootSettings, logDeprecation); + + sinon.assert.calledOnce(getSchema); + sinon.assert.calledWithExactly(getSchema, pluginSpec); + + sinon.assert.calledOnce(config.extendSchema); + sinon.assert.calledWithExactly(config.extendSchema, schema, rootSettings.foo.bar, configPrefix); + }); + + it('adds the schema for a plugin spec to its config prefix', async () => { + const config = Config.withDefaultSchema(); + expect(config.has('foo.bar.baz')).to.be(false); + await extendConfigService(pluginSpec, config); + expect(config.has('foo.bar.baz')).to.be(true); + }); + + it('initializes it with the default settings', async () => { + const config = Config.withDefaultSchema(); + await extendConfigService(pluginSpec, config); + expect(config.get('foo.bar.baz.enabled')).to.be(true); + expect(config.get('foo.bar.baz.test')).to.be('bonk'); + }); + + it('initializes it with values from root settings if defined', async () => { + const config = Config.withDefaultSchema(); + await extendConfigService(pluginSpec, config, { + foo: { + bar: { + baz: { + test: 'hello world' + } + } + } + }); + + expect(config.get('foo.bar.baz.test')).to.be('hello world'); + }); + + it('throws if root settings are invalid', async () => { + const config = Config.withDefaultSchema(); + try { + await extendConfigService(pluginSpec, config, { + foo: { + bar: { + baz: { + test: { + 'not a string': true + } + } + } + } + }); + throw new Error('Expected extendConfigService() to throw because of bad settings'); + } catch (error) { + expect(error.message).to.contain('"test" must be a string'); + } + }); + + it('calls logDeprecation() with deprecation messages', async () => { + const config = Config.withDefaultSchema(); + const logDeprecation = sinon.stub(); + await extendConfigService(pluginSpec, config, { + foo: { + bar: { + baz: { + oldTest: '123' + } + } + } + }, logDeprecation); + sinon.assert.calledOnce(logDeprecation); + sinon.assert.calledWithExactly(logDeprecation, sinon.match('"oldTest" is deprecated')); + }); + + it('uses settings after transforming deprecations', async () => { + const config = Config.withDefaultSchema(); + await extendConfigService(pluginSpec, config, { + foo: { + bar: { + baz: { + oldTest: '123' + } + } + } + }); + expect(config.get('foo.bar.baz.test')).to.be('123'); + }); + }); + + describe('disableConfigExtension()', () => { + it('removes added config', async () => { + const config = Config.withDefaultSchema(); + await extendConfigService(pluginSpec, config); + expect(config.has('foo.bar.baz.test')).to.be(true); + await disableConfigExtension(pluginSpec, config); + expect(config.has('foo.bar.baz.test')).to.be(false); + }); + + it('leaves {configPrefix}.enabled config', async () => { + const config = Config.withDefaultSchema(); + expect(config.has('foo.bar.baz.enabled')).to.be(false); + await extendConfigService(pluginSpec, config); + expect(config.get('foo.bar.baz.enabled')).to.be(true); + await disableConfigExtension(pluginSpec, config); + expect(config.get('foo.bar.baz.enabled')).to.be(false); + }); + }); +}); diff --git a/src/plugin_discovery/plugin_config/__tests__/schema.js b/src/plugin_discovery/plugin_config/__tests__/schema.js new file mode 100644 index 00000000000000..a04e3304dd7584 --- /dev/null +++ b/src/plugin_discovery/plugin_config/__tests__/schema.js @@ -0,0 +1,72 @@ +import expect from 'expect.js'; + +import { PluginPack } from '../../plugin_pack'; +import { getSchema, getStubSchema } from '../schema'; + +describe('plugin discovery/schema', () => { + function createPluginSpec(configProvider) { + return new PluginPack({ + path: '/dev/null', + pkg: { + name: 'test', + version: 'kibana', + }, + provider: ({ Plugin }) => new Plugin({ + configPrefix: 'foo.bar.baz', + config: configProvider, + }), + }) + .getPluginSpecs() + .pop(); + } + + describe('getSchema()', () => { + it('calls the config provider and returns its return value', async () => { + const pluginSpec = createPluginSpec(() => 'foo'); + expect(await getSchema(pluginSpec)).to.be('foo'); + }); + + it('supports config provider that returns a promise', async () => { + const pluginSpec = createPluginSpec(() => Promise.resolve('foo')); + expect(await getSchema(pluginSpec)).to.be('foo'); + }); + + it('uses default schema when no config provider', async () => { + const schema = await getSchema(createPluginSpec()); + expect(schema).to.be.an('object'); + expect(schema).to.have.property('validate').a('function'); + expect(schema.validate({}).value).to.eql({ + enabled: true + }); + }); + + it('uses default schema when config returns falsy value', async () => { + const schema = await getSchema(createPluginSpec(() => null)); + expect(schema).to.be.an('object'); + expect(schema).to.have.property('validate').a('function'); + expect(schema.validate({}).value).to.eql({ + enabled: true + }); + }); + + it('uses default schema when config promise resolves to falsy value', async () => { + const schema = await getSchema(createPluginSpec(() => Promise.resolve(null))); + expect(schema).to.be.an('object'); + expect(schema).to.have.property('validate').a('function'); + expect(schema.validate({}).value).to.eql({ + enabled: true + }); + }); + }); + + describe('getStubSchema()', () => { + it('returns schema with enabled: false', async () => { + const schema = await getStubSchema(); + expect(schema).to.be.an('object'); + expect(schema).to.have.property('validate').a('function'); + expect(schema.validate({}).value).to.eql({ + enabled: false + }); + }); + }); +}); diff --git a/src/plugin_discovery/plugin_config/__tests__/settings.js b/src/plugin_discovery/plugin_config/__tests__/settings.js new file mode 100644 index 00000000000000..8883cbf95a057b --- /dev/null +++ b/src/plugin_discovery/plugin_config/__tests__/settings.js @@ -0,0 +1,64 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; + +import { PluginPack } from '../../plugin_pack'; +import { getSettings } from '../settings'; + +describe('plugin_discovery/settings', () => { + const pluginSpec = new PluginPack({ + path: '/dev/null', + pkg: { + name: 'test', + version: 'kibana', + }, + provider: ({ Plugin }) => new Plugin({ + configPrefix: 'a.b.c', + deprecations: ({ rename }) => [ + rename('foo', 'bar') + ] + }), + }) + .getPluginSpecs() + .pop(); + + describe('getSettings()', () => { + it('reads settings from config prefix', async () => { + const rootSettings = { + a: { + b: { + c: { + enabled: false + } + } + } + }; + + expect(await getSettings(pluginSpec, rootSettings)) + .to.eql({ + enabled: false + }); + }); + + it('allows rootSettings to be undefined', async () => { + expect(await getSettings(pluginSpec)) + .to.eql(undefined); + }); + + it('resolves deprecations', async () => { + const logDeprecation = sinon.stub(); + expect(await getSettings(pluginSpec, { + a: { + b: { + c: { + foo: true + } + } + } + }, logDeprecation)).to.eql({ + bar: true + }); + + sinon.assert.calledOnce(logDeprecation); + }); + }); +}); From 99c1ff1d904f0fae3910ee7392c22aef5a6c8a15 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 22:24:34 -0700 Subject: [PATCH 51/67] typo --- src/plugin_discovery/plugin_pack/pack_at_path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin_discovery/plugin_pack/pack_at_path.js b/src/plugin_discovery/plugin_pack/pack_at_path.js index 646d4abe447b99..ca84f3745870af 100644 --- a/src/plugin_discovery/plugin_pack/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/pack_at_path.js @@ -21,7 +21,7 @@ async function createPackAtPath(path) { const pkg = require(pkgPath); if (!pkg || typeof pkg !== 'object') { - throw createInvalidPackError(path, 'must have a value package.json file'); + throw createInvalidPackError(path, 'must have a valid package.json file'); } let provider = require(path); From 88328a4bc8302c63ce953809fa0c6eb51e23692f Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 4 Dec 2017 22:42:04 -0700 Subject: [PATCH 52/67] [uiExports/readme] fix link --- src/ui/ui_exports/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/ui_exports/README.md b/src/ui/ui_exports/README.md index 05a142e598f9d2..ab81febe679938 100644 --- a/src/ui/ui_exports/README.md +++ b/src/ui/ui_exports/README.md @@ -86,10 +86,10 @@ This reducer format was chosen so that it will be easier to look back at these r ### defaults -The [./ui_export_defaults][UiExportDefaults] module defines the default shape of the uiExports object produced by `collectUiExports()`. The defaults generally describe the `uiExports` from the UI System itself, like default visTypes and such. +The [`ui_exports/ui_export_defaults`][UiExportDefaults] module defines the default shape of the uiExports object produced by `collectUiExports()`. The defaults generally describe the `uiExports` from the UI System itself, like default visTypes and such. [UiApp]: ../ui_apps/ui_app.js "UiApp class definition" -[UiExportTypes]: ./ui_export_defaults.js "uiExport defaults definition" +[UiExportDefaults]: ./ui_export_defaults.js "uiExport defaults definition" [UiExportTypes]: ./ui_export_types/index.js "Index of default ui_export_types module" [UiAppExportType]: ./ui_export_types/ui_apps.js "UiApp extension type definition" [PluginSpec]: ../../plugin_discovery/plugin_spec/plugin_spec.js "PluginSpec class definition" From 3091c0aa77262421f474c52d5255452fcf842dd3 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 11:51:38 -0700 Subject: [PATCH 53/67] [pluginDiscovery/packAtPath] fail with InvalidPackError if path is not a string --- src/plugin_discovery/plugin_pack/pack_at_path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin_discovery/plugin_pack/pack_at_path.js b/src/plugin_discovery/plugin_pack/pack_at_path.js index ca84f3745870af..cde04d631a8f51 100644 --- a/src/plugin_discovery/plugin_pack/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/pack_at_path.js @@ -6,7 +6,7 @@ import { isDirectory, isFile } from './lib'; import { PluginPack } from './plugin_pack'; async function createPackAtPath(path) { - if (!isAbsolute(path)) { + if (typeof path !== 'string' || !isAbsolute(path)) { throw createInvalidPackError(null, 'requires an absolute path'); } From 0e2f131fe5616ec78310cf11ff9691d929656f65 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 11:52:10 -0700 Subject: [PATCH 54/67] [pluginDiscovery/packAtPath] rely on error.code to detect missing package.json file --- src/plugin_discovery/plugin_pack/lib/fs.js | 9 --------- src/plugin_discovery/plugin_pack/lib/index.js | 1 - src/plugin_discovery/plugin_pack/pack_at_path.js | 13 ++++++++----- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/plugin_discovery/plugin_pack/lib/fs.js b/src/plugin_discovery/plugin_pack/lib/fs.js index 9422f87ec08666..9010169eca21dd 100644 --- a/src/plugin_discovery/plugin_pack/lib/fs.js +++ b/src/plugin_discovery/plugin_pack/lib/fs.js @@ -18,15 +18,6 @@ async function statTest(path, test) { return false; } -/** - * Determine if a path currently points to a file - * @param {String} path - * @return {Promise} - */ -export async function isFile(path) { - return await statTest(path, stat => stat.isFile()); -} - /** * Determine if a path currently points to a directory * @param {String} path diff --git a/src/plugin_discovery/plugin_pack/lib/index.js b/src/plugin_discovery/plugin_pack/lib/index.js index 9bfa5045bfa428..662ed0e5e1818c 100644 --- a/src/plugin_discovery/plugin_pack/lib/index.js +++ b/src/plugin_discovery/plugin_pack/lib/index.js @@ -1,5 +1,4 @@ export { - isFile, isDirectory, createChildDirectory$, } from './fs'; diff --git a/src/plugin_discovery/plugin_pack/pack_at_path.js b/src/plugin_discovery/plugin_pack/pack_at_path.js index cde04d631a8f51..a11cab20154011 100644 --- a/src/plugin_discovery/plugin_pack/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/pack_at_path.js @@ -2,7 +2,7 @@ import { Observable } from 'rxjs'; import { isAbsolute, resolve } from 'path'; import { createInvalidPackError } from '../errors'; -import { isDirectory, isFile } from './lib'; +import { isDirectory } from './lib'; import { PluginPack } from './plugin_pack'; async function createPackAtPath(path) { @@ -14,12 +14,15 @@ async function createPackAtPath(path) { throw createInvalidPackError(path, 'must be a directory'); } - const pkgPath = resolve(path, 'package.json'); - if (!await isFile(pkgPath)) { - throw createInvalidPackError(path, 'must have a package.json file'); + let pkg; + try { + pkg = require(resolve(path, 'package.json')); + } catch (error) { + if (error.code === 'MODULE_NOT_FOUND') { + throw createInvalidPackError(path, 'must have a package.json file'); + } } - const pkg = require(pkgPath); if (!pkg || typeof pkg !== 'object') { throw createInvalidPackError(path, 'must have a valid package.json file'); } From 36dd268b58a2bfe0492cd8df1581faf312d9232d Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 12:03:37 -0700 Subject: [PATCH 55/67] [pluginDiscovery/packAtPath] only attempt to get pack when observable is subscribed --- src/plugin_discovery/plugin_pack/pack_at_path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin_discovery/plugin_pack/pack_at_path.js b/src/plugin_discovery/plugin_pack/pack_at_path.js index a11cab20154011..c195c20c9754f0 100644 --- a/src/plugin_discovery/plugin_pack/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/pack_at_path.js @@ -39,7 +39,7 @@ async function createPackAtPath(path) { } export const createPackAtPath$ = (path) => ( - Observable.fromPromise(createPackAtPath(path)) + Observable.defer(() => createPackAtPath(path)) .map(pack => ({ pack })) .catch(error => [{ error }]) ); From c35ef963387bc0cd09ebb5b5a7afe059ea0da661 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 12:03:57 -0700 Subject: [PATCH 56/67] [pluginDiscovery/packAtPath] add tests --- .../fixtures/plugins/broken/package.json | 3 + .../fixtures/plugins/exports_number/index.js | 1 + .../plugins/exports_number/package.json | 4 ++ .../fixtures/plugins/exports_object/index.js | 3 + .../plugins/exports_object/package.json | 4 ++ .../fixtures/plugins/exports_string/index.js | 1 + .../plugins/exports_string/package.json | 4 ++ .../__tests__/fixtures/plugins/foo/index.js | 5 ++ .../fixtures/plugins/foo/package.json | 4 ++ .../__tests__/fixtures/plugins/index.js | 1 + .../__tests__/fixtures/plugins/lib/index.js | 1 + .../__tests__/fixtures/plugins/lib/my_lib.js | 3 + .../fixtures/plugins/prebuilt/index.js | 14 ++++ .../fixtures/plugins/prebuilt/package.json | 3 + .../plugin_pack/__tests__/pack_at_path.js | 67 +++++++++++++++++++ 15 files changed, 118 insertions(+) create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js create mode 100644 src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json create mode 100644 src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json new file mode 100644 index 00000000000000..f830e8b60c02d1 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json @@ -0,0 +1,3 @@ +{ + "name": +} diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js new file mode 100644 index 00000000000000..aef22247d75263 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js @@ -0,0 +1 @@ +export default 1; diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json new file mode 100644 index 00000000000000..e43c2f0bc984cf --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "version": "kibana" +} diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js new file mode 100644 index 00000000000000..7cf8282359b41c --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js @@ -0,0 +1,3 @@ +export default { + foo: 'bar' +}; diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json new file mode 100644 index 00000000000000..e43c2f0bc984cf --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "version": "kibana" +} diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js new file mode 100644 index 00000000000000..d02ba545bd3b38 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js @@ -0,0 +1 @@ +export default 'foo'; diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json new file mode 100644 index 00000000000000..e43c2f0bc984cf --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "version": "kibana" +} diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js new file mode 100644 index 00000000000000..cbb05e0deed528 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js @@ -0,0 +1,5 @@ +module.exports = function (kibana) { + return new kibana.Plugin({ + id: 'foo', + }); +}; diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json new file mode 100644 index 00000000000000..e43c2f0bc984cf --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "version": "kibana" +} diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js new file mode 100644 index 00000000000000..6be02374db118b --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js @@ -0,0 +1 @@ +console.log('hello world'); diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js new file mode 100644 index 00000000000000..32d8cd85937889 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js @@ -0,0 +1 @@ +export { myLib } from './my_lib'; diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js new file mode 100644 index 00000000000000..db2df02b5b7435 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js @@ -0,0 +1,3 @@ +export function myLib() { + console.log('lib'); +} diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js new file mode 100644 index 00000000000000..89744b2dd3fd9c --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js @@ -0,0 +1,14 @@ +/* eslint-disable */ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +exports.default = function (_ref) { + var Plugin = _ref.Plugin; + + return new Plugin({ + id: 'foo' + }); +}; diff --git a/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json new file mode 100644 index 00000000000000..b1b74e0e76b129 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json @@ -0,0 +1,3 @@ +{ + "name": "prebuilt" +} diff --git a/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js b/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js new file mode 100644 index 00000000000000..93cf2368f476be --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js @@ -0,0 +1,67 @@ +import { resolve } from 'path'; +import { inspect } from 'util'; + +import expect from 'expect.js'; + +import { createPackAtPath$ } from '../pack_at_path'; +import { PluginPack } from '../plugin_pack'; +import { isInvalidPackError } from '../../errors'; + +const PLUGINS = resolve(__dirname, 'fixtures/plugins'); + +describe('plugin discovery/plugin_pack', () => { + describe('createPackAtPath$()', () => { + it('returns an observable', () => { + expect(createPackAtPath$()) + .to.have.property('subscribe').a('function'); + }); + it('gets the default provider from prebuilt babel modules', async () => { + const results = await createPackAtPath$(resolve(PLUGINS, 'prebuilt')).toArray().toPromise(); + expect(results).to.have.length(1); + expect(results[0]).to.only.have.keys(['pack']); + expect(results[0].pack).to.be.a(PluginPack); + }); + describe('errors emitted as { error } results', () => { + async function checkError(path, check) { + const results = await createPackAtPath$(path).toArray().toPromise(); + expect(results).to.have.length(1); + expect(results[0]).to.only.have.keys(['error']); + const { error } = results[0]; + if (!isInvalidPackError(error)) { + throw new Error(`Expected ${inspect(error)} to be an 'InvalidPackError'`); + } + await check(error); + } + it('undefined path', () => checkError(undefined, error => { + expect(error.message).to.contain('requires an absolute path'); + })); + it('relative path', () => checkError('plugins/foo', error => { + expect(error.message).to.contain('requires an absolute path'); + })); + it('./relative path', () => checkError('./plugins/foo', error => { + expect(error.message).to.contain('requires an absolute path'); + })); + it('non-existant path', () => checkError(resolve(PLUGINS, 'baz'), error => { + expect(error.message).to.contain('must be a directory'); + })); + it('path to a file', () => checkError(resolve(PLUGINS, 'index.js'), error => { + expect(error.message).to.contain('must be a directory'); + })); + it('directory without a package.json', () => checkError(resolve(PLUGINS, 'lib'), error => { + expect(error.message).to.contain('must have a package.json file'); + })); + it('directory with an invalid package.json', () => checkError(resolve(PLUGINS, 'broken'), error => { + expect(error.message).to.contain('must have a valid package.json file'); + })); + it('default export is an object', () => checkError(resolve(PLUGINS, 'exports_object'), error => { + expect(error.message).to.contain('must export a function'); + })); + it('default export is an number', () => checkError(resolve(PLUGINS, 'exports_number'), error => { + expect(error.message).to.contain('must export a function'); + })); + it('default export is an string', () => checkError(resolve(PLUGINS, 'exports_string'), error => { + expect(error.message).to.contain('must export a function'); + })); + }); + }); +}); From 0d870c50550930c24e871931e244fb306824ba8f Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 12:20:01 -0700 Subject: [PATCH 57/67] [pluginDiscovery/pluginPack] move absolute path checks into fs lib --- .../plugin_pack/__tests__/pack_at_path.js | 33 +++++++++++++++---- src/plugin_discovery/plugin_pack/lib/fs.js | 18 ++++++++-- .../plugin_pack/pack_at_path.js | 6 +--- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js b/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js index 93cf2368f476be..815619c7354be3 100644 --- a/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js @@ -5,10 +5,22 @@ import expect from 'expect.js'; import { createPackAtPath$ } from '../pack_at_path'; import { PluginPack } from '../plugin_pack'; -import { isInvalidPackError } from '../../errors'; +import { isInvalidPackError, isInvalidDirectoryError } from '../../errors'; const PLUGINS = resolve(__dirname, 'fixtures/plugins'); +function assertInvalidDirectoryError(error) { + if (!isInvalidDirectoryError(error)) { + throw new Error(`Expected ${inspect(error)} to be an 'InvalidDirectoryError'`); + } +} + +function assertInvalidPackError(error) { + if (!isInvalidPackError(error)) { + throw new Error(`Expected ${inspect(error)} to be an 'InvalidPackError'`); + } +} + describe('plugin discovery/plugin_pack', () => { describe('createPackAtPath$()', () => { it('returns an observable', () => { @@ -27,39 +39,46 @@ describe('plugin discovery/plugin_pack', () => { expect(results).to.have.length(1); expect(results[0]).to.only.have.keys(['error']); const { error } = results[0]; - if (!isInvalidPackError(error)) { - throw new Error(`Expected ${inspect(error)} to be an 'InvalidPackError'`); - } await check(error); } it('undefined path', () => checkError(undefined, error => { - expect(error.message).to.contain('requires an absolute path'); + assertInvalidDirectoryError(error); + expect(error.message).to.contain('path must be a string'); })); it('relative path', () => checkError('plugins/foo', error => { - expect(error.message).to.contain('requires an absolute path'); + assertInvalidDirectoryError(error); + expect(error.message).to.contain('path must be absolute'); })); it('./relative path', () => checkError('./plugins/foo', error => { - expect(error.message).to.contain('requires an absolute path'); + assertInvalidDirectoryError(error); + expect(error.message).to.contain('path must be absolute'); })); it('non-existant path', () => checkError(resolve(PLUGINS, 'baz'), error => { + assertInvalidPackError(error); expect(error.message).to.contain('must be a directory'); })); it('path to a file', () => checkError(resolve(PLUGINS, 'index.js'), error => { + assertInvalidPackError(error); expect(error.message).to.contain('must be a directory'); })); it('directory without a package.json', () => checkError(resolve(PLUGINS, 'lib'), error => { + assertInvalidPackError(error); expect(error.message).to.contain('must have a package.json file'); })); it('directory with an invalid package.json', () => checkError(resolve(PLUGINS, 'broken'), error => { + assertInvalidPackError(error); expect(error.message).to.contain('must have a valid package.json file'); })); it('default export is an object', () => checkError(resolve(PLUGINS, 'exports_object'), error => { + assertInvalidPackError(error); expect(error.message).to.contain('must export a function'); })); it('default export is an number', () => checkError(resolve(PLUGINS, 'exports_number'), error => { + assertInvalidPackError(error); expect(error.message).to.contain('must export a function'); })); it('default export is an string', () => checkError(resolve(PLUGINS, 'exports_string'), error => { + assertInvalidPackError(error); expect(error.message).to.contain('must export a function'); })); }); diff --git a/src/plugin_discovery/plugin_pack/lib/fs.js b/src/plugin_discovery/plugin_pack/lib/fs.js index 9010169eca21dd..d04037d15e069c 100644 --- a/src/plugin_discovery/plugin_pack/lib/fs.js +++ b/src/plugin_discovery/plugin_pack/lib/fs.js @@ -1,11 +1,21 @@ import { stat, readdir } from 'fs'; -import { resolve } from 'path'; +import { resolve, isAbsolute } from 'path'; import { fromNode as fcb } from 'bluebird'; import { Observable } from 'rxjs'; import { createInvalidDirectoryError } from '../../errors'; +function assertAbsolutePath(path) { + if (typeof path !== 'string') { + throw createInvalidDirectoryError(new TypeError('path must be a string'), path); + } + + if (!isAbsolute(path)) { + throw createInvalidDirectoryError(new TypeError('path must be absolute'), path); + } +} + async function statTest(path, test) { try { const stats = await fcb(cb => stat(path, cb)); @@ -24,6 +34,7 @@ async function statTest(path, test) { * @return {Promise} */ export async function isDirectory(path) { + assertAbsolutePath(path); return await statTest(path, stat => stat.isDirectory()); } @@ -34,7 +45,10 @@ export async function isDirectory(path) { */ export const createChildDirectory$ = (path) => ( Observable - .fromPromise(fcb(cb => readdir(path, cb))) + .defer(() => { + assertAbsolutePath(path); + return fcb(cb => readdir(path, cb)); + }) .catch(error => { throw createInvalidDirectoryError(error, path); }) diff --git a/src/plugin_discovery/plugin_pack/pack_at_path.js b/src/plugin_discovery/plugin_pack/pack_at_path.js index c195c20c9754f0..906a0772c11961 100644 --- a/src/plugin_discovery/plugin_pack/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/pack_at_path.js @@ -1,15 +1,11 @@ import { Observable } from 'rxjs'; -import { isAbsolute, resolve } from 'path'; +import { resolve } from 'path'; import { createInvalidPackError } from '../errors'; import { isDirectory } from './lib'; import { PluginPack } from './plugin_pack'; async function createPackAtPath(path) { - if (typeof path !== 'string' || !isAbsolute(path)) { - throw createInvalidPackError(null, 'requires an absolute path'); - } - if (!await isDirectory(path)) { throw createInvalidPackError(path, 'must be a directory'); } From 75dbffee3f3b0cc4e73d4eb36d4255ad9e129704 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 12:20:40 -0700 Subject: [PATCH 58/67] [pluginDiscovery/packsInDirectory] fix error type check --- src/plugin_discovery/plugin_pack/packs_in_directory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin_discovery/plugin_pack/packs_in_directory.js b/src/plugin_discovery/plugin_pack/packs_in_directory.js index ad24743367f0b6..7078fab4f32f78 100644 --- a/src/plugin_discovery/plugin_pack/packs_in_directory.js +++ b/src/plugin_discovery/plugin_pack/packs_in_directory.js @@ -23,7 +23,7 @@ export const createPacksInDirectory$ = (path) => ( // this error is produced by createChildDirectory$() when the path // is invalid, we return them as an error result similar to how // createPackAtPath$ works when it finds invalid packs in a directory - if (isInvalidDirectoryError(path)) { + if (isInvalidDirectoryError(error)) { return [{ error }]; } From b9b75d4adb5853f855c9ed0503f793209e0bcc5a Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 12:32:44 -0700 Subject: [PATCH 59/67] [pluginDiscovery/pluginPack/tests] share some utils --- .../plugin_pack/__tests__/pack_at_path.js | 36 +++++++------------ .../plugin_pack/__tests__/utils.js | 18 ++++++++++ 2 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 src/plugin_discovery/plugin_pack/__tests__/utils.js diff --git a/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js b/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js index 815619c7354be3..8fe4de898b76df 100644 --- a/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js @@ -1,25 +1,15 @@ import { resolve } from 'path'; -import { inspect } from 'util'; import expect from 'expect.js'; import { createPackAtPath$ } from '../pack_at_path'; import { PluginPack } from '../plugin_pack'; -import { isInvalidPackError, isInvalidDirectoryError } from '../../errors'; +import { + PLUGINS_DIR, + assertInvalidPackError, + assertInvalidDirectoryError +} from './utils'; -const PLUGINS = resolve(__dirname, 'fixtures/plugins'); - -function assertInvalidDirectoryError(error) { - if (!isInvalidDirectoryError(error)) { - throw new Error(`Expected ${inspect(error)} to be an 'InvalidDirectoryError'`); - } -} - -function assertInvalidPackError(error) { - if (!isInvalidPackError(error)) { - throw new Error(`Expected ${inspect(error)} to be an 'InvalidPackError'`); - } -} describe('plugin discovery/plugin_pack', () => { describe('createPackAtPath$()', () => { @@ -28,7 +18,7 @@ describe('plugin discovery/plugin_pack', () => { .to.have.property('subscribe').a('function'); }); it('gets the default provider from prebuilt babel modules', async () => { - const results = await createPackAtPath$(resolve(PLUGINS, 'prebuilt')).toArray().toPromise(); + const results = await createPackAtPath$(resolve(PLUGINS_DIR, 'prebuilt')).toArray().toPromise(); expect(results).to.have.length(1); expect(results[0]).to.only.have.keys(['pack']); expect(results[0].pack).to.be.a(PluginPack); @@ -53,31 +43,31 @@ describe('plugin discovery/plugin_pack', () => { assertInvalidDirectoryError(error); expect(error.message).to.contain('path must be absolute'); })); - it('non-existant path', () => checkError(resolve(PLUGINS, 'baz'), error => { + it('non-existant path', () => checkError(resolve(PLUGINS_DIR, 'baz'), error => { assertInvalidPackError(error); expect(error.message).to.contain('must be a directory'); })); - it('path to a file', () => checkError(resolve(PLUGINS, 'index.js'), error => { + it('path to a file', () => checkError(resolve(PLUGINS_DIR, 'index.js'), error => { assertInvalidPackError(error); expect(error.message).to.contain('must be a directory'); })); - it('directory without a package.json', () => checkError(resolve(PLUGINS, 'lib'), error => { + it('directory without a package.json', () => checkError(resolve(PLUGINS_DIR, 'lib'), error => { assertInvalidPackError(error); expect(error.message).to.contain('must have a package.json file'); })); - it('directory with an invalid package.json', () => checkError(resolve(PLUGINS, 'broken'), error => { + it('directory with an invalid package.json', () => checkError(resolve(PLUGINS_DIR, 'broken'), error => { assertInvalidPackError(error); expect(error.message).to.contain('must have a valid package.json file'); })); - it('default export is an object', () => checkError(resolve(PLUGINS, 'exports_object'), error => { + it('default export is an object', () => checkError(resolve(PLUGINS_DIR, 'exports_object'), error => { assertInvalidPackError(error); expect(error.message).to.contain('must export a function'); })); - it('default export is an number', () => checkError(resolve(PLUGINS, 'exports_number'), error => { + it('default export is an number', () => checkError(resolve(PLUGINS_DIR, 'exports_number'), error => { assertInvalidPackError(error); expect(error.message).to.contain('must export a function'); })); - it('default export is an string', () => checkError(resolve(PLUGINS, 'exports_string'), error => { + it('default export is an string', () => checkError(resolve(PLUGINS_DIR, 'exports_string'), error => { assertInvalidPackError(error); expect(error.message).to.contain('must export a function'); })); diff --git a/src/plugin_discovery/plugin_pack/__tests__/utils.js b/src/plugin_discovery/plugin_pack/__tests__/utils.js new file mode 100644 index 00000000000000..00a692b843745f --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/utils.js @@ -0,0 +1,18 @@ +import { resolve } from 'path'; +import { inspect } from 'util'; + +import { isInvalidPackError, isInvalidDirectoryError } from '../../errors'; + +export const PLUGINS_DIR = resolve(__dirname, 'fixtures/plugins'); + +export function assertInvalidDirectoryError(error) { + if (!isInvalidDirectoryError(error)) { + throw new Error(`Expected ${inspect(error)} to be an 'InvalidDirectoryError'`); + } +} + +export function assertInvalidPackError(error) { + if (!isInvalidPackError(error)) { + throw new Error(`Expected ${inspect(error)} to be an 'InvalidPackError'`); + } +} From 3dbe93e7657ce0571697216e277e02d9f113a48a Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 14:14:28 -0700 Subject: [PATCH 60/67] [pluginDiscovery/packsInDirectory] add tests --- .../__tests__/packs_in_directory.js | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/plugin_discovery/plugin_pack/__tests__/packs_in_directory.js diff --git a/src/plugin_discovery/plugin_pack/__tests__/packs_in_directory.js b/src/plugin_discovery/plugin_pack/__tests__/packs_in_directory.js new file mode 100644 index 00000000000000..df46ee9edbd2a1 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/packs_in_directory.js @@ -0,0 +1,69 @@ +import { resolve } from 'path'; + +import expect from 'expect.js'; + +import { createPacksInDirectory$ } from '../packs_in_directory'; +import { PluginPack } from '../plugin_pack'; + +import { + PLUGINS_DIR, + assertInvalidDirectoryError, + assertInvalidPackError, +} from './utils'; + +describe('plugin discovery/packs in directory', () => { + describe('createPacksInDirectory$()', () => { + describe('errors emitted as { error } results', () => { + async function checkError(path, check) { + const results = await createPacksInDirectory$(path).toArray().toPromise(); + expect(results).to.have.length(1); + expect(results[0]).to.only.have.keys('error'); + const { error } = results[0]; + await check(error); + } + + it('undefined path', () => checkError(undefined, error => { + assertInvalidDirectoryError(error); + expect(error.message).to.contain('path must be a string'); + })); + it('relative path', () => checkError('my/plugins', error => { + assertInvalidDirectoryError(error); + expect(error.message).to.contain('path must be absolute'); + })); + it('./relative path', () => checkError('./my/pluginsd', error => { + assertInvalidDirectoryError(error); + expect(error.message).to.contain('path must be absolute'); + })); + it('non-existant path', () => checkError(resolve(PLUGINS_DIR, 'notreal'), error => { + assertInvalidDirectoryError(error); + expect(error.message).to.contain('no such file or directory'); + })); + it('path to a file', () => checkError(resolve(PLUGINS_DIR, 'index.js'), error => { + assertInvalidDirectoryError(error); + expect(error.message).to.contain('not a directory'); + })); + }); + + it('includes child errors for invalid packs within a valid directory', async () => { + const results = await createPacksInDirectory$(PLUGINS_DIR).toArray().toPromise(); + + const errors = results + .map(result => result.error) + .filter(Boolean); + + const packs = results + .map(result => result.pack) + .filter(Boolean); + + errors.forEach(assertInvalidPackError); + packs.forEach(pack => expect(pack).to.be.a(PluginPack)); + // there should be one result for each item in PLUGINS_DIR + expect(results).to.have.length(8); + // six of the fixtures are errors of some sorta + expect(errors).to.have.length(6); + // two of them are valid + expect(packs).to.have.length(2); + + }); + }); +}); From 17912cf7144742aa72efd47808a5cfd5688f9715 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 14:25:59 -0700 Subject: [PATCH 61/67] [pluginDiscovery/pluginPack] only cast undefined to array --- src/plugin_discovery/plugin_pack/plugin_pack.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugin_discovery/plugin_pack/plugin_pack.js b/src/plugin_discovery/plugin_pack/plugin_pack.js index 21b1563601e861..61f109e14b748e 100644 --- a/src/plugin_discovery/plugin_pack/plugin_pack.js +++ b/src/plugin_discovery/plugin_pack/plugin_pack.js @@ -40,7 +40,8 @@ export class PluginPack { } }; - const specs = [].concat(this._provider(api) || []); + const result = this._provider(api); + const specs = [].concat(result === undefined ? [] : result); // verify that all specs are instances of passed "Plugin" class specs.forEach(spec => { From 139e2b44e2c18c2e9b76c2330456cc94eb38b380 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 14:26:11 -0700 Subject: [PATCH 62/67] [pluginDiscovery/pluginPack] add tests --- .../plugin_pack/__tests__/plugin_pack.js | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/plugin_discovery/plugin_pack/__tests__/plugin_pack.js diff --git a/src/plugin_discovery/plugin_pack/__tests__/plugin_pack.js b/src/plugin_discovery/plugin_pack/__tests__/plugin_pack.js new file mode 100644 index 00000000000000..bb8ca5a7a48bc9 --- /dev/null +++ b/src/plugin_discovery/plugin_pack/__tests__/plugin_pack.js @@ -0,0 +1,110 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; + +import { PluginPack } from '../plugin_pack'; +import { PluginSpec } from '../../plugin_spec'; + +describe('plugin discovery/plugin pack', () => { + describe('constructor', () => { + it('requires an object', () => { + expect(() => { + new PluginPack(); + }).to.throwError(); + }); + }); + describe('#getPkg()', () => { + it('returns the `pkg` constructor argument', () => { + const pkg = {}; + const pack = new PluginPack({ pkg }); + expect(pack.getPkg()).to.be(pkg); + }); + }); + describe('#getPath()', () => { + it('returns the `path` constructor argument', () => { + const path = {}; + const pack = new PluginPack({ path }); + expect(pack.getPath()).to.be(path); + }); + }); + describe('#getPluginSpecs()', () => { + it('calls the `provider` constructor argument with an api including a single sub class of PluginSpec', () => { + const provider = sinon.stub(); + const pack = new PluginPack({ provider }); + sinon.assert.notCalled(provider); + pack.getPluginSpecs(); + sinon.assert.calledOnce(provider); + sinon.assert.calledWithExactly(provider, { + Plugin: sinon.match(Class => { + return Class.prototype instanceof PluginSpec; + }, 'Subclass of PluginSpec') + }); + }); + + it('casts undefined return value to array', () => { + const pack = new PluginPack({ provider: () => undefined }); + expect(pack.getPluginSpecs()).to.eql([]); + }); + + it('casts single PluginSpec to an array', () => { + const pack = new PluginPack({ + path: '/dev/null', + pkg: { name: 'foo', version: 'kibana' }, + provider: ({ Plugin }) => new Plugin({}) + }); + + const specs = pack.getPluginSpecs(); + expect(specs).to.be.an('array'); + expect(specs).to.have.length(1); + expect(specs[0]).to.be.a(PluginSpec); + }); + + it('returns an array of PluginSpec', () => { + const pack = new PluginPack({ + path: '/dev/null', + pkg: { name: 'foo', version: 'kibana' }, + provider: ({ Plugin }) => [ + new Plugin({}), + new Plugin({}), + ] + }); + + const specs = pack.getPluginSpecs(); + expect(specs).to.be.an('array'); + expect(specs).to.have.length(2); + expect(specs[0]).to.be.a(PluginSpec); + expect(specs[1]).to.be.a(PluginSpec); + }); + + it('throws if non-undefined return value is not an instance of api.Plugin', () => { + let OtherPluginSpecClass; + const otherPack = new PluginPack({ + path: '/dev/null', + pkg: { name: 'foo', version: 'kibana' }, + provider: (api) => { + OtherPluginSpecClass = api.Plugin; + } + }); + + // call getPluginSpecs() on other pack to get it's api.Plugin class + otherPack.getPluginSpecs(); + + const badPacks = [ + new PluginPack({ provider: () => false }), + new PluginPack({ provider: () => null }), + new PluginPack({ provider: () => 1 }), + new PluginPack({ provider: () => 'true' }), + new PluginPack({ provider: () => true }), + new PluginPack({ provider: () => new Date() }), + new PluginPack({ provider: () => /foo.*bar/ }), + new PluginPack({ provider: () => function () {} }), + new PluginPack({ provider: () => new OtherPluginSpecClass({}) }), + ]; + + for (const pack of badPacks) { + expect(() => pack.getPluginSpecs()).to.throwError(error => { + expect(error.message).to.contain('unexpected plugin export'); + }); + } + }); + }); +}); From 66e53df5b8c160fdda20a1068aeecebd9ce92bd5 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 14:33:23 -0700 Subject: [PATCH 63/67] [pluginDiscovery/pluginSpec/isVersionCompatible] add tests --- .../__tests__/is_version_compatible.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js diff --git a/src/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js b/src/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js new file mode 100644 index 00000000000000..f8788f0f458c04 --- /dev/null +++ b/src/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js @@ -0,0 +1,29 @@ +import expect from 'expect.js'; + +import { isVersionCompatible } from '../is_version_compatible'; + +describe('plugin discovery/plugin spec', () => { + describe('isVersionCompatible()', () => { + const tests = [ + ['kibana', '6.0.0', true], + ['kibana', '6.0.0-rc1', true], + ['6.0.0-rc1', '6.0.0', true], + ['6.0.0', '6.0.0-rc1', true], + ['6.0.0-rc2', '6.0.0-rc1', true], + ['6.0.0-rc2', '6.0.0-rc3', true], + ['foo', 'bar', false], + ['6.0.0', '5.1.4', false], + ['5.1.4', '6.0.0', false], + ['5.1.4-SNAPSHOT', '6.0.0-rc2-SNAPSHOT', false], + ['5.1.4', '6.0.0-rc2-SNAPSHOT', false], + ['5.1.4-SNAPSHOT', '6.0.0', false], + ['5.1.4-SNAPSHOT', '6.0.0-rc2', false], + ]; + + for (const [plugin, kibana, shouldPass] of tests) { + it(`${shouldPass ? 'should' : `shouldn't`} allow plugin: ${plugin} kibana: ${kibana}`, () => { + expect(isVersionCompatible(plugin, kibana)).to.be(shouldPass); + }); + } + }); +}); From 03de1917f2e908aad5e64a0e90937f4cdfbec70a Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 14:40:19 -0700 Subject: [PATCH 64/67] [pluginDiscovery/InvalidPluginError] be less redundant --- src/plugin_discovery/errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin_discovery/errors.js b/src/plugin_discovery/errors.js index 65efa02c93aecf..d96014812bc881 100644 --- a/src/plugin_discovery/errors.js +++ b/src/plugin_discovery/errors.js @@ -38,7 +38,7 @@ export function isInvalidPackError(error) { */ const ERROR_INVALID_PLUGIN = 'ERROR_INVALID_PLUGIN'; export function createInvalidPluginError(spec, reason) { - const error = new Error(`Plugin from ${spec.getId()} from ${spec.getPack().getPath()} is invalid because ${reason}`); + const error = new Error(`Plugin from ${spec.getId()} at ${spec.getPack().getPath()} is invalid because ${reason}`); error[errorCodeProperty] = ERROR_INVALID_PLUGIN; error.spec = spec; return error; From 14076402c8f0558d0efb965a8bfed40607d1b6b1 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 16:36:32 -0700 Subject: [PATCH 65/67] [pluginDiscovery/pluginSpec] verify config service is passed to isEnabled() --- src/plugin_discovery/plugin_spec/plugin_spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugin_discovery/plugin_spec/plugin_spec.js b/src/plugin_discovery/plugin_spec/plugin_spec.js index 0e3681d5976f43..40b9c32571c6d0 100644 --- a/src/plugin_discovery/plugin_spec/plugin_spec.js +++ b/src/plugin_discovery/plugin_spec/plugin_spec.js @@ -106,6 +106,10 @@ export class PluginSpec { } isEnabled(config) { + if (!config || typeof config.get !== 'function' || typeof config.has !== 'function') { + throw new TypeError('PluginSpec#isEnabled() must be called with a config service'); + } + if (this._isEnabled) { return this._isEnabled(config); } From bad97a86987a28b5b186e025037195fd0e4b2544 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 17:17:16 -0700 Subject: [PATCH 66/67] [pluginDiscovery/pluginSpec] add tests --- .../plugin_spec/__tests__/plugin_spec.js | 473 ++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 src/plugin_discovery/plugin_spec/__tests__/plugin_spec.js diff --git a/src/plugin_discovery/plugin_spec/__tests__/plugin_spec.js b/src/plugin_discovery/plugin_spec/__tests__/plugin_spec.js new file mode 100644 index 00000000000000..2550e30c6501c7 --- /dev/null +++ b/src/plugin_discovery/plugin_spec/__tests__/plugin_spec.js @@ -0,0 +1,473 @@ +import { resolve } from 'path'; + +import expect from 'expect.js'; +import sinon from 'sinon'; + +import { PluginPack } from '../../plugin_pack'; +import { PluginSpec } from '../plugin_spec'; +import * as IsVersionCompatibleNS from '../is_version_compatible'; + +const fooPack = new PluginPack({ + path: '/dev/null', + pkg: { name: 'foo', version: 'kibana' }, +}); + +describe('plugin discovery/plugin spec', () => { + describe('PluginSpec', () => { + describe('validation', () => { + it('throws if missing spec.id AND Pack has no name', () => { + const pack = new PluginPack({ pkg: {} }); + expect(() => new PluginSpec(pack, {})).to.throwError(error => { + expect(error.message).to.contain('Unable to determine plugin id'); + }); + }); + + it('throws if missing spec.kibanaVersion AND Pack has no version', () => { + const pack = new PluginPack({ pkg: { name: 'foo' } }); + expect(() => new PluginSpec(pack, {})).to.throwError(error => { + expect(error.message).to.contain('Unable to determine plugin version'); + }); + }); + + it('throws if spec.require is defined, but not an array', () => { + function assert(require) { + expect(() => new PluginSpec(fooPack, { require })).to.throwError(error => { + expect(error.message).to.contain('"plugin.require" must be an array of plugin ids'); + }); + } + + assert(null); + assert(''); + assert('kibana'); + assert(1); + assert(0); + assert(/a.*b/); + }); + + it('throws if spec.publicDir is truthy and not a string', () => { + function assert(publicDir) { + expect(() => new PluginSpec(fooPack, { publicDir })).to.throwError(error => { + expect(error.message).to.contain('Path must be a string'); + }); + } + + assert(1); + assert(function () {}); + assert([]); + assert(/a.*b/); + }); + + it('throws if spec.publicDir is not an absolute path', () => { + function assert(publicDir) { + expect(() => new PluginSpec(fooPack, { publicDir })).to.throwError(error => { + expect(error.message).to.contain('plugin.publicDir must be an absolute path'); + }); + } + + assert('relative/path'); + assert('./relative/path'); + }); + + it('throws if spec.publicDir basename is not `public`', () => { + function assert(publicDir) { + expect(() => new PluginSpec(fooPack, { publicDir })).to.throwError(error => { + expect(error.message).to.contain('must end with a "public" directory'); + }); + } + + assert('/www'); + assert('/www/'); + assert('/www/public/my_plugin'); + assert('/www/public/my_plugin/'); + }); + }); + + describe('#getPack()', () => { + it('returns the pack', () => { + const spec = new PluginSpec(fooPack, {}); + expect(spec.getPack()).to.be(fooPack); + }); + }); + + describe('#getPkg()', () => { + it('returns the pkg from the pack', () => { + const spec = new PluginSpec(fooPack, {}); + expect(spec.getPkg()).to.be(fooPack.getPkg()); + }); + }); + + describe('#getPath()', () => { + it('returns the path from the pack', () => { + const spec = new PluginSpec(fooPack, {}); + expect(spec.getPath()).to.be(fooPack.getPath()); + }); + }); + + describe('#getId()', () => { + it('uses spec.id', () => { + const spec = new PluginSpec(fooPack, { + id: 'bar' + }); + + expect(spec.getId()).to.be('bar'); + }); + + it('defaults to pack.pkg.name', () => { + const spec = new PluginSpec(fooPack, {}); + + expect(spec.getId()).to.be('foo'); + }); + }); + + describe('#getVerison()', () => { + it('uses spec.version', () => { + const spec = new PluginSpec(fooPack, { + version: 'bar' + }); + + expect(spec.getVersion()).to.be('bar'); + }); + + it('defaults to pack.pkg.version', () => { + const spec = new PluginSpec(fooPack, {}); + + expect(spec.getVersion()).to.be('kibana'); + }); + }); + + describe('#isEnabled()', () => { + describe('spec.isEnabled is not defined', () => { + function setup(configPrefix, configGetImpl) { + const spec = new PluginSpec(fooPack, { configPrefix }); + const config = { + get: sinon.spy(configGetImpl), + has: sinon.stub() + }; + + return { spec, config }; + } + + it('throws if not passed a config service', () => { + const { spec } = setup('a.b.c', () => true); + + expect(() => spec.isEnabled()).to.throwError(error => { + expect(error.message).to.contain('must be called with a config service'); + }); + expect(() => spec.isEnabled(null)).to.throwError(error => { + expect(error.message).to.contain('must be called with a config service'); + }); + expect(() => spec.isEnabled({ get: () => {} })).to.throwError(error => { + expect(error.message).to.contain('must be called with a config service'); + }); + }); + + it('returns true when config.get([...configPrefix, "enabled"]) returns true', () => { + const { spec, config } = setup('d.e.f', () => true); + + expect(spec.isEnabled(config)).to.be(true); + sinon.assert.calledOnce(config.get); + sinon.assert.calledWithExactly(config.get, ['d', 'e', 'f', 'enabled']); + }); + + it('returns false when config.get([...configPrefix, "enabled"]) returns false', () => { + const { spec, config } = setup('g.h.i', () => false); + + expect(spec.isEnabled(config)).to.be(false); + sinon.assert.calledOnce(config.get); + sinon.assert.calledWithExactly(config.get, ['g', 'h', 'i', 'enabled']); + }); + }); + + describe('spec.isEnabled is defined', () => { + function setup(isEnabledImpl) { + const isEnabled = sinon.spy(isEnabledImpl); + const spec = new PluginSpec(fooPack, { isEnabled }); + const config = { + get: sinon.stub(), + has: sinon.stub() + }; + + return { isEnabled, spec, config }; + } + + it('throws if not passed a config service', () => { + const { spec } = setup(() => true); + + expect(() => spec.isEnabled()).to.throwError(error => { + expect(error.message).to.contain('must be called with a config service'); + }); + expect(() => spec.isEnabled(null)).to.throwError(error => { + expect(error.message).to.contain('must be called with a config service'); + }); + expect(() => spec.isEnabled({ get: () => {} })).to.throwError(error => { + expect(error.message).to.contain('must be called with a config service'); + }); + }); + + it('does not check config if spec.isEnabled returns true', () => { + const { spec, isEnabled, config } = setup(() => true); + + expect(spec.isEnabled(config)).to.be(true); + sinon.assert.calledOnce(isEnabled); + sinon.assert.notCalled(config.get); + }); + + it('does not check config if spec.isEnabled returns false', () => { + const { spec, isEnabled, config } = setup(() => false); + + expect(spec.isEnabled(config)).to.be(false); + sinon.assert.calledOnce(isEnabled); + sinon.assert.notCalled(config.get); + }); + }); + }); + + describe('#getExpectedKibanaVersion()', () => { + describe('has: spec.kibanaVersion,pkg.kibana.version,spec.version,pkg.version', () => { + it('uses spec.kibanaVersion', () => { + const pack = new PluginPack({ + path: '/dev/null', + pkg: { + name: 'expkv', + version: '1.0.0', + kibana: { + version: '6.0.0' + } + } + }); + + const spec = new PluginSpec(pack, { + version: '2.0.0', + kibanaVersion: '5.0.0' + }); + + expect(spec.getExpectedKibanaVersion()).to.be('5.0.0'); + }); + }); + describe('missing: spec.kibanaVersion, has: pkg.kibana.version,spec.version,pkg.version', () => { + it('uses pkg.kibana.version', () => { + const pack = new PluginPack({ + path: '/dev/null', + pkg: { + name: 'expkv', + version: '1.0.0', + kibana: { + version: '6.0.0' + } + } + }); + + const spec = new PluginSpec(pack, { + version: '2.0.0', + }); + + expect(spec.getExpectedKibanaVersion()).to.be('6.0.0'); + }); + }); + describe('missing: spec.kibanaVersion,pkg.kibana.version, has: spec.version,pkg.version', () => { + it('uses spec.version', () => { + const pack = new PluginPack({ + path: '/dev/null', + pkg: { + name: 'expkv', + version: '1.0.0', + } + }); + + const spec = new PluginSpec(pack, { + version: '2.0.0', + }); + + expect(spec.getExpectedKibanaVersion()).to.be('2.0.0'); + }); + }); + describe('missing: spec.kibanaVersion,pkg.kibana.version,spec.version, has: pkg.version', () => { + it('uses pkg.version', () => { + const pack = new PluginPack({ + path: '/dev/null', + pkg: { + name: 'expkv', + version: '1.0.0', + } + }); + + const spec = new PluginSpec(pack, {}); + + expect(spec.getExpectedKibanaVersion()).to.be('1.0.0'); + }); + }); + }); + + describe('#isVersionCompatible()', () => { + it('passes this.getExpectedKibanaVersion() and arg to isVersionCompatible(), returns its result', () => { + const spec = new PluginSpec(fooPack, { version: '1.0.0' }); + sinon.stub(spec, 'getExpectedKibanaVersion').returns('foo'); + const isVersionCompatible = sinon.stub(IsVersionCompatibleNS, 'isVersionCompatible').returns('bar'); + expect(spec.isVersionCompatible('baz')).to.be('bar'); + + sinon.assert.calledOnce(spec.getExpectedKibanaVersion); + sinon.assert.calledWithExactly(spec.getExpectedKibanaVersion); + + sinon.assert.calledOnce(isVersionCompatible); + sinon.assert.calledWithExactly(isVersionCompatible, 'foo', 'baz'); + }); + }); + + describe('#getRequiredPluginIds()', () => { + it('returns spec.require', () => { + const spec = new PluginSpec(fooPack, { require: [1, 2, 3] }); + expect(spec.getRequiredPluginIds()).to.eql([1, 2, 3]); + }); + }); + + describe('#getPublicDir()', () => { + describe('spec.publicDir === false', () => { + it('returns null', () => { + const spec = new PluginSpec(fooPack, { publicDir: false }); + expect(spec.getPublicDir()).to.be(null); + }); + }); + + describe('spec.publicDir is falsy', () => { + it('returns public child of pack path', () => { + function assert(publicDir) { + const spec = new PluginSpec(fooPack, { publicDir }); + expect(spec.getPublicDir()).to.be(resolve('/dev/null/public')); + } + + assert(0); + assert(''); + assert(null); + assert(undefined); + assert(NaN); + }); + }); + + describe('spec.publicDir is an absolute path', () => { + it('returns the path', () => { + const spec = new PluginSpec(fooPack, { + publicDir: '/var/www/public' + }); + + expect(spec.getPublicDir()).to.be('/var/www/public'); + }); + }); + + // NOTE: see constructor tests for other truthy-tests that throw in constructor + }); + + describe('#getExportSpecs()', () => { + it('returns spec.uiExports', () => { + const spec = new PluginSpec(fooPack, { + uiExports: 'foo' + }); + + expect(spec.getExportSpecs()).to.be('foo'); + }); + }); + + describe('#getPreInitHandler()', () => { + it('returns spec.preInit', () => { + const spec = new PluginSpec(fooPack, { + preInit: 'foo' + }); + + expect(spec.getPreInitHandler()).to.be('foo'); + }); + }); + + describe('#getInitHandler()', () => { + it('returns spec.init', () => { + const spec = new PluginSpec(fooPack, { + init: 'foo' + }); + + expect(spec.getInitHandler()).to.be('foo'); + }); + }); + + describe('#getConfigPrefix()', () => { + describe('spec.configPrefix is truthy', () => { + it('returns spec.configPrefix', () => { + const spec = new PluginSpec(fooPack, { + configPrefix: 'foo.bar.baz' + }); + + expect(spec.getConfigPrefix()).to.be('foo.bar.baz'); + }); + }); + describe('spec.configPrefix is falsy', () => { + it('returns spec.getId()', () => { + function assert(configPrefix) { + const spec = new PluginSpec(fooPack, { configPrefix }); + sinon.stub(spec, 'getId').returns('foo'); + expect(spec.getConfigPrefix()).to.be('foo'); + sinon.assert.calledOnce(spec.getId); + } + + assert(false); + assert(null); + assert(undefined); + assert(''); + assert(0); + }); + }); + }); + + describe('#getConfigSchemaProvider()', () => { + it('returns spec.config', () => { + const spec = new PluginSpec(fooPack, { + config: 'foo' + }); + + expect(spec.getConfigSchemaProvider()).to.be('foo'); + }); + }); + + describe('#readConfigValue()', () => { + const spec = new PluginSpec(fooPack, { + configPrefix: 'foo.bar' + }); + + const config = { + get: sinon.stub() + }; + + afterEach(() => config.get.reset()); + + describe('key = "foo"', () => { + it('passes key as own array item', () => { + spec.readConfigValue(config, 'foo'); + sinon.assert.calledOnce(config.get); + sinon.assert.calledWithExactly(config.get, ['foo', 'bar', 'foo']); + }); + }); + + describe('key = "foo.bar"', () => { + it('passes key as two array items', () => { + spec.readConfigValue(config, 'foo.bar'); + sinon.assert.calledOnce(config.get); + sinon.assert.calledWithExactly(config.get, ['foo', 'bar', 'foo', 'bar']); + }); + }); + + describe('key = ["foo", "bar"]', () => { + it('merged keys into array', () => { + spec.readConfigValue(config, ['foo', 'bar']); + sinon.assert.calledOnce(config.get); + sinon.assert.calledWithExactly(config.get, ['foo', 'bar', 'foo', 'bar']); + }); + }); + }); + + describe('#getDeprecationsProvider()', () => { + it('returns spec.deprecations', () => { + const spec = new PluginSpec(fooPack, { + deprecations: 'foo' + }); + + expect(spec.getDeprecationsProvider()).to.be('foo'); + }); + }); + }); +}); From a0479810ab28cc10f6617dc9ef060be39cef9f8f Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 5 Dec 2017 17:21:17 -0700 Subject: [PATCH 67/67] fix "existent" spelling --- src/core_plugins/kibana/server/lib/manage_uuid.js | 2 +- src/optimize/bundles_route/__tests__/bundles_route.js | 2 +- src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js | 2 +- .../plugin_pack/__tests__/packs_in_directory.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core_plugins/kibana/server/lib/manage_uuid.js b/src/core_plugins/kibana/server/lib/manage_uuid.js index 355e3e4a970449..83bf4fd06d6d07 100644 --- a/src/core_plugins/kibana/server/lib/manage_uuid.js +++ b/src/core_plugins/kibana/server/lib/manage_uuid.js @@ -17,7 +17,7 @@ export default async function manageUuid(server) { return result.toString(FILE_ENCODING); } catch (err) { if (err.code === 'ENOENT') { - // non-existant uuid file is ok + // non-existent uuid file is ok return false; } server.log(['error', 'read-uuid'], err); diff --git a/src/optimize/bundles_route/__tests__/bundles_route.js b/src/optimize/bundles_route/__tests__/bundles_route.js index 623c7a7f0d74a2..a38f5a28a17899 100644 --- a/src/optimize/bundles_route/__tests__/bundles_route.js +++ b/src/optimize/bundles_route/__tests__/bundles_route.js @@ -219,7 +219,7 @@ describe('optimizer/bundle route', () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/non_existant.js' + url: '/bundles/non_existent.js' }); expect(response.statusCode).to.be(404); diff --git a/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js b/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js index 8fe4de898b76df..1f8e63fbd2e0d7 100644 --- a/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js +++ b/src/plugin_discovery/plugin_pack/__tests__/pack_at_path.js @@ -43,7 +43,7 @@ describe('plugin discovery/plugin_pack', () => { assertInvalidDirectoryError(error); expect(error.message).to.contain('path must be absolute'); })); - it('non-existant path', () => checkError(resolve(PLUGINS_DIR, 'baz'), error => { + it('non-existent path', () => checkError(resolve(PLUGINS_DIR, 'baz'), error => { assertInvalidPackError(error); expect(error.message).to.contain('must be a directory'); })); diff --git a/src/plugin_discovery/plugin_pack/__tests__/packs_in_directory.js b/src/plugin_discovery/plugin_pack/__tests__/packs_in_directory.js index df46ee9edbd2a1..fcd3e96428744d 100644 --- a/src/plugin_discovery/plugin_pack/__tests__/packs_in_directory.js +++ b/src/plugin_discovery/plugin_pack/__tests__/packs_in_directory.js @@ -34,7 +34,7 @@ describe('plugin discovery/packs in directory', () => { assertInvalidDirectoryError(error); expect(error.message).to.contain('path must be absolute'); })); - it('non-existant path', () => checkError(resolve(PLUGINS_DIR, 'notreal'), error => { + it('non-existent path', () => checkError(resolve(PLUGINS_DIR, 'notreal'), error => { assertInvalidDirectoryError(error); expect(error.message).to.contain('no such file or directory'); }));