From 6ce9168205f1300d3352aa4247ff4f8dabdc4986 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Wed, 6 Feb 2019 21:18:51 -0500 Subject: [PATCH 01/46] migration guide up to services --- src/core/MIGRATION.md | 121 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/core/MIGRATION.md diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md new file mode 100644 index 0000000000000..ea283328e7bd4 --- /dev/null +++ b/src/core/MIGRATION.md @@ -0,0 +1,121 @@ +# Migrating legacy plugins to the new platform + +* Overview + * Architectural + * Services + * Integrating with other plugins + * Legacy plugin problem areas + * Browser vs server +* Plan of action + * TypeScript + * De-angular + * Architectural changes with legacy "shim" + * Switch to new platform services + * Migrate to the new platform + +Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. + +The goal of this document is to guide teams through the recommended process of migrating at a high level. Every plugin is different, so teams should tweak this plan based on their unique requirements. + +We'll start with an overview of how plugins work in the new platform, and we'll end with a generic plan of action that can be applied to any plugin in the repo today. + +## Overview + +Plugins in the new platform are not especially novel or complicated to describe. Our intention wasn't to build some clever system that magically solved problems through abstractions and layers of obscurity, and we wanted to make sure plugins could continue to use most of the same technologies they use today, at least from a technical perspective. + +New platform plugins exist in the `src/plugins` and `x-pack/plugins` directories. + +### Architecture + +Plugins are defined as classes and exposed to the platform itself through a simple wrapper function. A plugin can have browser side code, server side code, or both. There is no architectural difference between a plugin in the browser and a plugin on the server, which is to say that in both places you describe your plugin similarly, and you interact with core and/or other plugins in the same way. + +The basic file structure of a new platform plugin named "demo" that had both client-side and server-side code would be: + +``` +src/plugins + demo + kibana.json [1] + public + index.ts [2] + plugin.ts [3] + server + index.ts [4] + plugin.ts [5] +``` + +**[1] `kibana.json`** is a static manifest file that is used to identify the plugin and to determine what kind of code the platform should execute from the plugin: + +```json +{ + "id": "demo", + "server": true, + "ui": true +} +``` + +Note that `package.json` files are irrelevant to and ignored by the new platform. + +**[2] `public/index.ts`** is the entry point into the client-side code of this plugin. It must export a function named `plugin`, which will receive a standard set of core capabilities as an argument (e.g. logger). It should return an instance of its plugin definition for the platform to register at load time. + +```ts +import { PluginInitializerContext } from '../../../core/public'; +import { Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} +``` + +**[3] `public/plugin.ts`** is the client-side plugin definition itself. Technically speaking it does not need to be a class or even a separate file from the entry point, but _all plugins at Elastic_ should be consistent in this way. + +```ts +import { PluginInitializerContext, PluginName, PluginStart, PluginStop } from '../../../core/public'; + +export class Plugin { + constructor(initializerContext: PluginInitializerContext) { + } + + public start(core: PluginStart, dependencies: Record) { + // called when plugin is started up, aka when Kibana is loaded + } + + public stop(core: PluginStop, dependencies: Record) { + // called when plugin is torn down, aka window.onbeforeunload + } +} +``` + +**[4] `server/index.ts`** is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: + +```ts +import { PluginInitializerContext } from '../../../core/server'; +import { Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} +``` + +**[5] `server/plugin.ts`** is the server-side plugin definition. The _shape_ of this plugin is the same as it's client-side counter-part: + +```ts +import { PluginInitializerContext, PluginName, PluginStart, PluginStop } from '../../../core/server'; + +export class Plugin { + constructor(initializerContext: PluginInitializerContext) { + } + + public start(core: PluginStart, dependencies: Record) { + // called when plugin is started up during Kibana's startup sequence + } + + public stop(core: PluginStop, dependencies: Record) { + // called when plugin is torn down during Kibana's shutdown sequence + } +} +``` + +The platform does not impose any technical restrictions on how the internals of the plugin are architected, though there are certain considerations related to how plugins interact with core and how plugins interact with other plugins that may greatly impact how they are built. + +### Services + From aca2423bfec2799dafdc791105f2c215b702f66a Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Wed, 6 Feb 2019 22:06:38 -0500 Subject: [PATCH 02/46] expand on services --- src/core/MIGRATION.md | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index ea283328e7bd4..b910e4c707f64 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -119,3 +119,92 @@ The platform does not impose any technical restrictions on how the internals of ### Services +The various independent domains that make up `core` are represented by a series of services, and many of those services expose public interfaces that are provided to _all_ plugins via the first argument of their `start` and `stop` functions. The interface varies from service to service, but it is always accessed through this argument. + +For example, the core `UiSettings` service exposes a function `get` to all plugin `start` functions. To use this function to retrieve a specific UI setting, a plugin just accesses it off of the first argument: + +```ts +import { PluginName, PluginStart } from '../../../core/public'; + +export class Plugin { + public start(core: PluginStart, dependencies: Record) { + core.uiSettings.get('courier:maxShardsBeforeCryTime'); + } +} +``` + +Different service interfaces can and will be passed to `start` and `stop` because certain functionality makes sense in the context of a running plugin while other types of functionality may have restrictions or may only make sense in the context of a plugin that is stopping. + +For example, the `stop` function in the browser gets invoked as part of the `window.onbeforeunload` event, which means you can't necessarily execute asynchronous code here in a reliable way. For that reason, `core` likely wouldn't provide any asynchronous functions to plugin `stop` functions in the browser. + +### Integrating with other plugins + +Plugins can expose public interfaces for other plugins to consume. Like `core`, those interfaces are bound to `start` and/or `stop`. + +Anything returned from `start` or `stop` will act as the interface, and while not a technical requirement, all Elastic plugins should expose types for that interface as well. + +**foobar plugin.ts:** + +```ts +export interface FoobarPluginStart { + getFoo(): string +} + +export interface FoobarPluginStop { + getBar(): string +} + +export class Plugin { + public start(): FoobarPluginStart { + return { + getFoo() { + return 'foo'; + } + }; + } + + public stop(): FoobarPluginStop { + getBar() { + return 'bar'; + } + } +} +``` + +Unlike core, capabilities exposed by plugins are _not_ automatically injected into all plugins. Instead, if a plugin wishes to use the public interface provided by another plugin, they must first declare that plugin as a dependency in their `kibana.json`. + +**demo kibana.json:** + +```json +{ + "id": "demo", + "requiredPlugins": [ + "foobar" + ], + "server": true, + "ui": true +} +``` + +With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of `start` and/or `stop`: + +**demo plugin.ts:** + +```ts +import { PluginName, PluginStart, PluginStop } from '../../../core/server'; +import { FoobarPluginStart, FoobarPluginStop } from '../../foobar/server'; + +export class Plugin { + public start(core: PluginStart, dependencies: Record) { + const { foobar } = dependencies; + foobar.getFoo(); // 'foo' + foobar.getBar(); // throws because getBar does not exist + } + + public stop(core: PluginStop, dependencies: Record) { + const { foobar } = dependencies; + foobar.getFoo(); // throws because getFoo does not exist + foobar.getBar(); // 'bar' + } +} +``` From b3ec36e7488a4fac3859a7bbc2b1fd9744942124 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 12 Feb 2019 15:28:06 -0500 Subject: [PATCH 03/46] challenges to overcome with legacy plugins --- src/core/MIGRATION.md | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index b910e4c707f64..4f27580c2be46 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -4,8 +4,7 @@ * Architectural * Services * Integrating with other plugins - * Legacy plugin problem areas - * Browser vs server + * Challenges to overcome with legacy plugins * Plan of action * TypeScript * De-angular @@ -194,17 +193,51 @@ With that specified in the plugin manifest, the appropriate interfaces are then import { PluginName, PluginStart, PluginStop } from '../../../core/server'; import { FoobarPluginStart, FoobarPluginStop } from '../../foobar/server'; +interface DemoStartDependencies { + foobar: FoobarPluginStart +} + +interface DemoStopDependencies { + foobar: FoobarPluginStop +} + export class Plugin { - public start(core: PluginStart, dependencies: Record) { + public start(core: PluginStart, dependencies: DemoStartDependencies) { const { foobar } = dependencies; foobar.getFoo(); // 'foo' foobar.getBar(); // throws because getBar does not exist } - public stop(core: PluginStop, dependencies: Record) { + public stop(core: PluginStop, dependencies: DemoStopDependencies) { const { foobar } = dependencies; foobar.getFoo(); // throws because getFoo does not exist foobar.getBar(); // 'bar' } } ``` + +### Challenges to overcome with legacy plugins + +New platform plugins have identical architecture in the browser and on the server. Legacy plugins have one architecture that they use in the browser and an entirely different architecture that they use on the server. + +This means that there are unique sets of challenges for migrating to the new platform depending on whether the legacy plugin code is on the server or in the browser. + +#### Challenges on the server + +The general shape/architecture of legacy server-side code is similar to the new platform architecture in one important way: most legacy server-side plugins define an `init` function where the bulk of their business logic begins, and they access both "core" and "plugin-provided" functionality through the arguments given to `init`. Rarely does legacy server-side code share stateful services via import statements. + +While not exactly the same, legacy plugin `init` functions behave similarly today as new platform `start` functions. There is no corresponding legacy concept of `stop`, however. + +Despite their similarities, server-side plugins pose a formidable challenge: legacy core and plugin functionality is retrieved from either the hapi.js `server` or `request` god objects. Worse, these objects are often passed deeply throughout entire plugins, which directly couples business logic with hapi. And the worst of it all is, these objects are mutable at any time. + +The key challenge to overcome with legacy server-side plugins will decoupling from hapi. + +#### Challenges in the browser + +The legacy plugin system in the browser is fundamentally incompatible with the new platform. There is no client-side plugin definition. There are no services that get passed to plugins at runtime. There really isn't even a concrete notion of "core". + +When a legacy browser plugin needs to access functionality from another plugin, say to register a UI section to render within another plugin, it imports a stateful (global singleton) JavaScript module and performs some sort of state mutation. Sometimes this module exists inside the plugin itself, and it gets imported via the `plugin/` webpack alias. Sometimes this module exists outside the context of plugins entirely and gets imported via the `ui/` webpack alias. Neither of these concepts exist in the new platform. + +Legacy browser plugins rely on the feature known as `uiExports/`, which integrates directly with our build system to ensure that plugin code is bundled together in such a way to enable that global singleton module state. There is no corresponding feature in the new platform, and in fact we intend down the line to build new platform plugins as immutable bundles that can not share state in this way. + +The key challenge to overcome with legacy browser-side plugins will be converting all imports from `plugin/`, `ui/`, `uiExports`, and relative imports from other plugins into a set of services that originate at runtime during plugin initialization and get passed around throughout the business logic of the plugin as function arguments. From 9d4ce9f160e5d737d7738213be77d935b1b93d19 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Wed, 13 Feb 2019 15:31:04 -0500 Subject: [PATCH 04/46] clean up type definitions --- src/core/MIGRATION.md | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 4f27580c2be46..07928ab0d7a5e 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -74,11 +74,11 @@ export class Plugin { constructor(initializerContext: PluginInitializerContext) { } - public start(core: PluginStart, dependencies: Record) { + public start(core: PluginStart) { // called when plugin is started up, aka when Kibana is loaded } - public stop(core: PluginStop, dependencies: Record) { + public stop(core: PluginStop) { // called when plugin is torn down, aka window.onbeforeunload } } @@ -104,11 +104,11 @@ export class Plugin { constructor(initializerContext: PluginInitializerContext) { } - public start(core: PluginStart, dependencies: Record) { + public start(core: PluginStart) { // called when plugin is started up during Kibana's startup sequence } - public stop(core: PluginStop, dependencies: Record) { + public stop(core: PluginStop) { // called when plugin is torn down during Kibana's shutdown sequence } } @@ -126,7 +126,7 @@ For example, the core `UiSettings` service exposes a function `get` to all plugi import { PluginName, PluginStart } from '../../../core/public'; export class Plugin { - public start(core: PluginStart, dependencies: Record) { + public start(core: PluginStart) { core.uiSettings.get('courier:maxShardsBeforeCryTime'); } } @@ -145,16 +145,11 @@ Anything returned from `start` or `stop` will act as the interface, and while no **foobar plugin.ts:** ```ts -export interface FoobarPluginStart { - getFoo(): string -} - -export interface FoobarPluginStop { - getBar(): string -} +export type FoobarPluginStart = ReturnType; +export type FoobarPluginStop = ReturnType; export class Plugin { - public start(): FoobarPluginStart { + public start() { return { getFoo() { return 'foo'; @@ -162,7 +157,7 @@ export class Plugin { }; } - public stop(): FoobarPluginStop { + public stop() { getBar() { return 'bar'; } From 20c35916c23f02021d3df100cd859caa566f826a Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Wed, 13 Feb 2019 17:24:05 -0500 Subject: [PATCH 05/46] being plan of action with typescript and angular stuff --- src/core/MIGRATION.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 07928ab0d7a5e..de06109fd3da4 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -236,3 +236,35 @@ When a legacy browser plugin needs to access functionality from another plugin, Legacy browser plugins rely on the feature known as `uiExports/`, which integrates directly with our build system to ensure that plugin code is bundled together in such a way to enable that global singleton module state. There is no corresponding feature in the new platform, and in fact we intend down the line to build new platform plugins as immutable bundles that can not share state in this way. The key challenge to overcome with legacy browser-side plugins will be converting all imports from `plugin/`, `ui/`, `uiExports`, and relative imports from other plugins into a set of services that originate at runtime during plugin initialization and get passed around throughout the business logic of the plugin as function arguments. + +### Plan of action + +In order to move a legacy plugin to the new plugin system, the challenges on the server and in the browser must be addressed. Fortunately, **the hardest problems can be solved in legacy plugins today** without consuming the new platform at all. + +At a high level, the bulk of the migration work can be broken down into two phases. + +First, refactor the plugin's architecture to isolate the legacy behaviors mentioned in the "Challenges to overcome with legacy plugins" section above. In practice, this involves moving all of the legacy imports and hapi god object references out of the business logic of your plugin and into a legacy _shim_. + +Second, update the consuming code of core services within the plugin to that of the new platform. This can be done in the legacy world, though it is dependent on the relevant services actually existing. + +Once those two things are done, the effort involved in actually updating your plugin to execute in the new plugin system is tiny and non-disruptive. + +Before you do any of that, there are two other things that will make all steps of this process a great deal easier and less risky: switch to TypeScript, and remove your dependencies on angular. + +#### TypeScript + +The new platform does not _require_ plugins to be built with TypeScript, but all subsequent steps of this plan of action are more straightforward and carry a great deal less risk if the code is already converted to TypeScript. + +TypeScript is a superset of JavaScript, so if your goal is the least possible effort, you can move to TypeScript with very few code changes mostly by adding `any` types all over the place. This isn't really any better than regular JavaScript, but simply having your code to `.ts` files means you can at least take advantage of the types that are exported from core and other plugins. This bare minimum approach won't help you much for the architectural shifts, but it could be a great help to you in reliably switching over to new platform services. + +### De-angular + +Angular is not a thing in the new platform. Hopefully your plugin began moving away from angular long ago, but if not, you're in a tight spot. + +If your plugin is registering some sort of global behavior that technically crosses application boundaries, then you have no choice but to get rid of angular. In this case, you're probably best off dealing with this before proceeding with the rest of action plan. + +If your plugin is using angular only in the context of its own application, then removing angular is likely not a definitive requirement for moving to the new platform. In this case, you will need to refactor your plugin to initialize an entirely standalone angular module that serves your application. You will need to create custom wrappers for any of the angular services you previously relied on (including those through `Private()`) inside your own plugin. + +At this poing, keeping angular around is not the recommended approach. If you feel you must do it, then talk to the platform team directly and we can help you craft a plan. + +We recommend that _all_ plugins treat moving away from angular as a top-most priority if they haven't done so already. From c3057bfff94ad27415d6d659ca5c148b42d0bb04 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 19 Feb 2019 15:22:09 -0500 Subject: [PATCH 06/46] faq in table of contents --- src/core/MIGRATION.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index de06109fd3da4..2b98f344b7cd5 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -11,6 +11,10 @@ * Architectural changes with legacy "shim" * Switch to new platform services * Migrate to the new platform +* Frequently asked questions + * How is static code shared between plugins? + * How is "common" code shared on both the client and server? + * When does code go into a plugin, core, or packages? Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. From 42e223d63f71463003494a568f4bf6ef5b770c80 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 19 Feb 2019 15:24:36 -0500 Subject: [PATCH 07/46] typo in typescript section --- src/core/MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 2b98f344b7cd5..1d60d07d8dbf8 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -259,7 +259,7 @@ Before you do any of that, there are two other things that will make all steps o The new platform does not _require_ plugins to be built with TypeScript, but all subsequent steps of this plan of action are more straightforward and carry a great deal less risk if the code is already converted to TypeScript. -TypeScript is a superset of JavaScript, so if your goal is the least possible effort, you can move to TypeScript with very few code changes mostly by adding `any` types all over the place. This isn't really any better than regular JavaScript, but simply having your code to `.ts` files means you can at least take advantage of the types that are exported from core and other plugins. This bare minimum approach won't help you much for the architectural shifts, but it could be a great help to you in reliably switching over to new platform services. +TypeScript is a superset of JavaScript, so if your goal is the least possible effort, you can move to TypeScript with very few code changes mostly by adding `any` types all over the place. This isn't really any better than regular JavaScript, but simply having your code in `.ts` files means you can at least take advantage of the types that are exported from core and other plugins. This bare minimum approach won't help you much for the architectural shifts, but it could be a great help to you in reliably switching over to new platform services. ### De-angular From af2afbf4f389b38277e5726600dc642cc79e6bd1 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 19 Feb 2019 20:56:38 -0500 Subject: [PATCH 08/46] wip architectural stuff --- src/core/MIGRATION.md | 79 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 1d60d07d8dbf8..e73e90b953193 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -72,7 +72,7 @@ export function plugin(initializerContext: PluginInitializerContext) { **[3] `public/plugin.ts`** is the client-side plugin definition itself. Technically speaking it does not need to be a class or even a separate file from the entry point, but _all plugins at Elastic_ should be consistent in this way. ```ts -import { PluginInitializerContext, PluginName, PluginStart, PluginStop } from '../../../core/public'; +import { PluginInitializerContext, PluginStart, PluginStop } from '../../../core/public'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { @@ -102,7 +102,7 @@ export function plugin(initializerContext: PluginInitializerContext) { **[5] `server/plugin.ts`** is the server-side plugin definition. The _shape_ of this plugin is the same as it's client-side counter-part: ```ts -import { PluginInitializerContext, PluginName, PluginStart, PluginStop } from '../../../core/server'; +import { PluginInitializerContext, PluginStart, PluginStop } from '../../../core/server'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { @@ -127,7 +127,7 @@ The various independent domains that make up `core` are represented by a series For example, the core `UiSettings` service exposes a function `get` to all plugin `start` functions. To use this function to retrieve a specific UI setting, a plugin just accesses it off of the first argument: ```ts -import { PluginName, PluginStart } from '../../../core/public'; +import { PluginStart } from '../../../core/public'; export class Plugin { public start(core: PluginStart) { @@ -189,7 +189,7 @@ With that specified in the plugin manifest, the appropriate interfaces are then **demo plugin.ts:** ```ts -import { PluginName, PluginStart, PluginStop } from '../../../core/server'; +import { PluginStart, PluginStop } from '../../../core/server'; import { FoobarPluginStart, FoobarPluginStop } from '../../foobar/server'; interface DemoStartDependencies { @@ -272,3 +272,74 @@ If your plugin is using angular only in the context of its own application, then At this poing, keeping angular around is not the recommended approach. If you feel you must do it, then talk to the platform team directly and we can help you craft a plan. We recommend that _all_ plugins treat moving away from angular as a top-most priority if they haven't done so already. + +### Architectural changes with legacy "shim" + +The bulk of the migration work for most plugins will be changing the way the plugin is architected so dependencies from core and other plugins flow in via the same entry point. This effort is relatively straightforward on the server, but it can be a tremendous undertaking for client-side code in some plugins. + +#### Server-side + +Legacy server-side plugins access functionality from core and other plugins at runtime via function arguments, which is similar to how they must be architected to use the new plugin system. + +Let's start with a legacy server-side plugin definition that exposes functionality for other plugins to consume and accesses functionality from both core and a different plugin. + +```ts +export default (kibana) => { + return new kibana.Plugin({ + id: 'demo_plugin', + + init(server) { + // access functionality exposed by core + server.route({ + path: '/api/demo_plugin/search', + method: 'POST', + async handler(request) { + const { elasticsearch } = server.plugins; + return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); + } + }); + + // creates an extension point that other plugins can call + server.expose('getDemoBar', () => { + // accesses functionality exposed by another plugin + return `Demo ${server.plugins.foo.getBar()}`; + }); + } + }); +} +``` + +If we were to express this same set of capabilities in a shape that's more suitable to the new plugin system, it would look something like this: + +```ts +import { PluginStart } from '../../core/server'; +import { FooPluginStart } from '../foo/server'; + +interface DemoStartDependencies { + foo: FooPluginStart +} + +export type DemoPluginStart = ReturnType; + +export class Plugin { + public start(core: PluginStart, dependencies: DemoStartDependencies) { + // access functionality exposed by core + core.http.route({ + path: '/api/demo_plugin/search', + method: 'POST', + async handler(request) { + const { elasticsearch } = core; // note, elasticsearch is moving to core + return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); + } + }); + + // creates an extension point that other plugins can call + return { + getDemoBar() { + // accesses functionality exposed by another plugin + return `Demo ${dependencies.foo.getBar()}`; + } + }; + } +} +``` From da594dee18960b794c8a4901a018af2c598473ec Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 22 Feb 2019 09:35:00 -0500 Subject: [PATCH 09/46] expanded explanation in architecture overhaul --- src/core/MIGRATION.md | 139 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 16 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index e73e90b953193..10412a19f937a 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -72,13 +72,13 @@ export function plugin(initializerContext: PluginInitializerContext) { **[3] `public/plugin.ts`** is the client-side plugin definition itself. Technically speaking it does not need to be a class or even a separate file from the entry point, but _all plugins at Elastic_ should be consistent in this way. ```ts -import { PluginInitializerContext, PluginStart, PluginStop } from '../../../core/public'; +import { PluginInitializerContext, CoreStart, PluginStop } from '../../../core/public'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { } - public start(core: PluginStart) { + public start(core: CoreStart) { // called when plugin is started up, aka when Kibana is loaded } @@ -102,13 +102,13 @@ export function plugin(initializerContext: PluginInitializerContext) { **[5] `server/plugin.ts`** is the server-side plugin definition. The _shape_ of this plugin is the same as it's client-side counter-part: ```ts -import { PluginInitializerContext, PluginStart, PluginStop } from '../../../core/server'; +import { PluginInitializerContext, CoreStart, PluginStop } from '../../../core/server'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { } - public start(core: PluginStart) { + public start(core: CoreStart) { // called when plugin is started up during Kibana's startup sequence } @@ -127,10 +127,10 @@ The various independent domains that make up `core` are represented by a series For example, the core `UiSettings` service exposes a function `get` to all plugin `start` functions. To use this function to retrieve a specific UI setting, a plugin just accesses it off of the first argument: ```ts -import { PluginStart } from '../../../core/public'; +import { CoreStart } from '../../../core/public'; export class Plugin { - public start(core: PluginStart) { + public start(core: CoreStart) { core.uiSettings.get('courier:maxShardsBeforeCryTime'); } } @@ -189,7 +189,7 @@ With that specified in the plugin manifest, the appropriate interfaces are then **demo plugin.ts:** ```ts -import { PluginStart, PluginStop } from '../../../core/server'; +import { CoreStart, PluginStop } from '../../../core/server'; import { FoobarPluginStart, FoobarPluginStop } from '../../foobar/server'; interface DemoStartDependencies { @@ -201,7 +201,7 @@ interface DemoStopDependencies { } export class Plugin { - public start(core: PluginStart, dependencies: DemoStartDependencies) { + public start(core: CoreStart, dependencies: DemoStartDependencies) { const { foobar } = dependencies; foobar.getFoo(); // 'foo' foobar.getBar(); // throws because getBar does not exist @@ -289,7 +289,6 @@ export default (kibana) => { id: 'demo_plugin', init(server) { - // access functionality exposed by core server.route({ path: '/api/demo_plugin/search', method: 'POST', @@ -299,9 +298,7 @@ export default (kibana) => { } }); - // creates an extension point that other plugins can call server.expose('getDemoBar', () => { - // accesses functionality exposed by another plugin return `Demo ${server.plugins.foo.getBar()}`; }); } @@ -312,7 +309,7 @@ export default (kibana) => { If we were to express this same set of capabilities in a shape that's more suitable to the new plugin system, it would look something like this: ```ts -import { PluginStart } from '../../core/server'; +import { CoreStart } from '../../core/server'; import { FooPluginStart } from '../foo/server'; interface DemoStartDependencies { @@ -322,8 +319,7 @@ interface DemoStartDependencies { export type DemoPluginStart = ReturnType; export class Plugin { - public start(core: PluginStart, dependencies: DemoStartDependencies) { - // access functionality exposed by core + public start(core: CoreStart, dependencies: DemoStartDependencies) { core.http.route({ path: '/api/demo_plugin/search', method: 'POST', @@ -333,13 +329,124 @@ export class Plugin { } }); - // creates an extension point that other plugins can call return { getDemoBar() { - // accesses functionality exposed by another plugin return `Demo ${dependencies.foo.getBar()}`; } }; } } ``` + +Let's break down the key differences in these examples. + +##### Defining the plugin + +The new plugin is defined as a class `Plugin`, whereas the legacy plugin exported a default factory function that instantiated a Kibana-supplied plugin class. Note that there is no id specified on the plugin class itself: + +```ts +// before +export default (kibana) => { + return new kibana.Plugin({ + // ... + }); +} + +// after +export class Plugin { + // ... +} +``` + +##### Starting the plugin up + +The new plugin definition uses `start` instead of `init`, and rather than getting the hapi server object as its only argument, it gets two arguments: the core services and a dependency on another plugin. + +```ts +// before +init(server) { + // ... +} + +//after +public start(core: CoreStart, dependencies: DemoStartDependencies) { + // ... +} +``` + +##### Accessing core services + +Rather than accessing "core" functions like HTTP routing directly on a hapi server object, the new plugin accesses core functionality through the top level services it exposes in the first argument to `start`. In the case of HTTP routing, it uses `core.http`. + +```ts +// before +server.route({ + path: '/api/demo_plugin/search', + method: 'POST', + async handler(request) { + const { elasticsearch } = server.plugins; + return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); + } +}); + +// after +core.http.route({ + path: '/api/demo_plugin/search', + method: 'POST', + async handler(request) { + const { elasticsearch } = core; // note, elasticsearch is moving to core + return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); + } +}); +``` + +##### Exposing services for other plugins + +Legacy plugins on the server might expose functionality or services to other plugins by invoking the `expose` function on the hapi `server` object. This can happen at any time throughout the runtime of Kibana which makes it less than reliable. + +New plugins return the contract (if any) that they wish to make available to downstream plugins. This ensures the entirety of a plugin's start contract is available upon completion of its own `start` function. It also makes it much easier to provide type definitions for plugin contracts. + +```ts +// before +server.expose('getDemoBar', () => { + // ... +}); + +// after +return { + getDemoBar() { + // ... + } +}; +``` + +##### Accessing plugin services + +In server-side code of legacy plugins, you once again use the hapi `server` object to access functionality that was exposed by other plugins. In new plugins, you access the exposed functionality in a similar way but on the second argument to `start` that is dedicated only to injecting plugin capabilities. + +```ts +// before +server.plugins.foo.getBar() + +// after +dependencies.foo.getBar() +``` + +##### Static files + +One other thing worth noting in this example is how a new plugin will consume static files from core or other plugins, and also how it will expose static files for other plugins. + +This is done through standard modules and relative imports. + +```ts +// import CoreStart type from core server +import { CoreStart } from '../../core/server'; + +// import FooPluginStart type from plugin foo +import { FooPluginStart } from '../foo/server'; + +// export DemoPluginStart type for downstream plugins, based on return value of start() +export type DemoPluginStart = ReturnType; +``` + +While these particular examples involve only types, the exact same pattern should be followed for those rare situations when a plugin exposes static functionality for plugins to consume. From 1f409ca89139a6da4f69a6981a8e8cde1d447655 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 22 Feb 2019 14:21:03 -0500 Subject: [PATCH 10/46] wip on client-side architecture changes --- src/core/MIGRATION.md | 66 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 10412a19f937a..38e41c54dd0b9 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -277,7 +277,7 @@ We recommend that _all_ plugins treat moving away from angular as a top-most pri The bulk of the migration work for most plugins will be changing the way the plugin is architected so dependencies from core and other plugins flow in via the same entry point. This effort is relatively straightforward on the server, but it can be a tremendous undertaking for client-side code in some plugins. -#### Server-side +#### Server-side architectural changes Legacy server-side plugins access functionality from core and other plugins at runtime via function arguments, which is similar to how they must be architected to use the new plugin system. @@ -450,3 +450,67 @@ export type DemoPluginStart = ReturnType; ``` While these particular examples involve only types, the exact same pattern should be followed for those rare situations when a plugin exposes static functionality for plugins to consume. + +##### Rule of thumb for server-side changes + +Outside of the temporary shim, does your plugin code rely directly on hapi.js? If not, you're probably good to go. + +#### Client-side architectural changes + +Client-side legacy plugin code is where things get weird, but the approach is largely the same - in the public entry file of the plugin, we separate the legacy integrations with the new plugin definition using a temporary "shim". + +As before, let's start with an example legacy client-side plugin. This example integrates with core, consumes functionality from another plugin, and exposes functionality for other plugins to consume via `uiExports`. This would be the rough shape of the code that would originate in the entry file, which would be either `index.ts` or `.ts`: + +```js +import chrome from 'ui/chrome'; +import routes from 'ui/routes'; + +import 'uiExports/demoExtensions'; +import { DemoExtensionsProvider } from 'ui/registry/demo_extensions'; + +import { getBar } from 'plugins/foo'; + +import template from './demo.html'; + +routes.enable(); +routes.when('/demo-foo', { + template, + controller($scope, config, indexPatterns, Private) { + const bar = getBar(); + $scope.demoBarUrl = chrome.addBasePath(`/demo/${bar}`); + $scope.extensions = Private(DemoExtensionsProvider); + }, +}); +``` + +Expressed in the shape of a new plugin: + +```ts +import { CoreStart } from '../../core/public'; +import { FooPluginStart } from '../foo/public'; + +interface DemoStartDependencies { + foo: FooPluginStart +} + +export type DemoPluginStart = ReturnType; + +export class Plugin { + public start(core: CoreStart, dependencies: DemoStartDependencies) { + core.router.when('/demo-foo', async (request) => { + + }); + + return { + registerExtension() { + + } + }; + } +} +``` + + +##### Rule of thumb for client-side changes + +Outside of the temporary shim, does your plugin code rely directly on code imported from webpack aliases (e.g. `import from 'plugins/...'` or `import from 'ui/...'`)? If not, you're probably good to go. From fb69d3a2e16689524b453825a623cb1ce7d7cc51 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 23 Feb 2019 13:12:46 -0500 Subject: [PATCH 11/46] scratch pad for client migration --- src/core/MIGRATION.md | 122 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 108 insertions(+), 14 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 38e41c54dd0b9..4d66d25463d6a 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -462,24 +462,118 @@ Client-side legacy plugin code is where things get weird, but the approach is la As before, let's start with an example legacy client-side plugin. This example integrates with core, consumes functionality from another plugin, and exposes functionality for other plugins to consume via `uiExports`. This would be the rough shape of the code that would originate in the entry file, which would be either `index.ts` or `.ts`: ```js -import chrome from 'ui/chrome'; -import routes from 'ui/routes'; +// visualize/hacks/plugin.js +// example init of updated plugin that preserves legacy API and introduces new one +import { startContracts } from 'ui/legacy/plugins'; +import { Plugin } from '../plugin'; -import 'uiExports/demoExtensions'; -import { DemoExtensionsProvider } from 'ui/registry/demo_extensions'; +const core = {}; +const dependencies = {} -import { getBar } from 'plugins/foo'; +const plugin = new Plugin(); +const start = plugin.start(core, dependencies); +startContracts.set('visualize', start); -import template from './demo.html'; +require('ui/registry/vis_types').__shimYourStuff__(plugin.visTypes$); +require('uiExports/visTypes'); -routes.enable(); -routes.when('/demo-foo', { - template, - controller($scope, config, indexPatterns, Private) { - const bar = getBar(); - $scope.demoBarUrl = chrome.addBasePath(`/demo/${bar}`); - $scope.extensions = Private(DemoExtensionsProvider); - }, + +// visualize/plugin.js +// example of upgraded plugin definition +export class Plugin { + constructor() { + this.visTypes$ = new ReplaySubject(1); + } + + start(core, dependencies) { + return { + registerVisType(type) { + this.visTypes$.push(type); + } + }; + } +} + + +// tag_cloud/hacks/plugin.js +// example init of updated plugin that consumes new interface +import { startContracts } from 'ui/legacy/plugins'; +import { Plugin } from '../plugin'; + +const core = {}; +const dependencies = { + ...startContracts +} + +const plugin = new Plugin(); +plugin.start(core, dependencies); + + + + + + + + + + + +// visualize/public/index.js +import { coreStart } from 'ui/core'; +import { foo } from 'ui/plugins'; + +import { visualize } from 'ui/legacy_plugins'; + +import { getBar } from 'plugins/bar'; + +const core = { + chrome: coreStart.chrome +}; + +const dependencies = { + bar: { + getBar + } +}; + +export class Plugin { + constructor() { + this.visTypes$ = new ReplaySubject(1); + } + start(core, dependencies) { + //require('ui/registry/vis_types').__shimYourStuff__(this.visTypes$); + require('uiExports/visTypes'); + + return { + visTypes$ = this.visTypes$.asObservable(); + myVis() { + + } + } + } +} + +export startContract = new Plugin(); + +//const start = new Plugin().start(core); + +export function myVis() { + return start.myVis(); +} + + + + +// my_custom_visualization/index.js +{ + uiExports: { + vis_type: 'plugins/my_custom_visulaization/some_file' + } +} +// my_custom_visualization/public/some_file.js +import { VisTypesProvider } from 'ui/registry/vis_types'; +VisTypesProvider.register(($http) => { + return { type: 'mycustomvis' }; }); ``` From c7a86b1c131af16114b0fa323d809427644bad8a Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sun, 24 Feb 2019 12:34:17 -0500 Subject: [PATCH 12/46] more client-side brainstorming --- src/core/MIGRATION.md | 108 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 4d66d25463d6a..38755b8eeac22 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -457,6 +457,24 @@ Outside of the temporary shim, does your plugin code rely directly on hapi.js? I #### Client-side architectural changes +Client-side legacy plugin code is where things get weird, but the approach is similar - a new plugin definition wraps the business logic of the plugin while legacy functionality is "shimmed" temporarily. Ultimately, there are three high levels goals for client-side architectural changes: + +1. Move all webpack alias imports (`ui/`, `plugin/`, `uiExports/`) into the root shim(s) +2. Adopt global new plugin definitions for all plugins +3. Source of truth for all stateful actions and configuration should originate from new plugin definition + +How you accomplish these things varies wildly depending on the plugin's current implementation and functionality. + +Every plugin will add their global plugin definition via a `hack` uiExport, which will ensure that the plugin definition is always loaded for all applications. This is inline with how the plugin service works in the new platform. + +Plugins that "own" a uiExport will move + + + +* Plugin without app +* Plugin with angular app +* Plugin with react app + Client-side legacy plugin code is where things get weird, but the approach is largely the same - in the public entry file of the plugin, we separate the legacy integrations with the new plugin definition using a temporary "shim". As before, let's start with an example legacy client-side plugin. This example integrates with core, consumes functionality from another plugin, and exposes functionality for other plugins to consume via `uiExports`. This would be the rough shape of the code that would originate in the entry file, which would be either `index.ts` or `.ts`: @@ -486,6 +504,16 @@ export class Plugin { } start(core, dependencies) { + core.applications.registerApp('visualize', (dom) => { + this.legacyHackApp(); + + import('../application').then(({ bootstrapApp }) => { + const app = bootstrapApp(dom); + }); + + return app.start(); + }); + return { registerVisType(type) { this.visTypes$.push(type); @@ -494,6 +522,86 @@ export class Plugin { } } +// visualize/index.js +// example of app entry file for upgraded angular plugin +import chrome from 'ui/chrome'; +import routes from 'ui/routes'; +import { uiModules } from 'ui/modules'; + +import 'uiExports/visTypes'; + +import 'ui/autoload/all'; +import './visualize'; +import 'ui/vislib'; +import { showAppRedirectNotification } from 'ui/notify'; + +routes.enable(); + +routes + .otherwise({ + redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}` + }); + +uiModules.get('kibana').run(showAppRedirectNotification); + + + +// example of plugin file for upgraded react plugin +// plugin +export class Plugin { + start(core, dependencies) { + const { I18nContext } = core.i18n; + core.applications.register('demo', async (dom) => { + const { mount } = await import('../application'); + return mount({ dom, I18nContext }); + }); + } +} + +// example of app entry file for upgraded react plugin +// application +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router-dom'; +import ReactDOM from 'react-dom'; + +import { configureStore } from './store'; +import { Main } from './components/Main'; + +import './style/global_overrides.css'; + +export function mount({ dom, I18nContext }) { + const store = configureStore(); + + ReactDOM.render( + + + +
+ + + , + dom + ); + + return function unmount() { + ReactDOM.unmountComponentAtNode(dom); + } +} + + +// entry +import { core } from 'ui/core'; +import { uiModules } from 'ui/modules'; // eslint-disable-line no-unused-vars +import 'ui/autoload/styles'; +import 'ui/autoload/all'; + +import template from './templates/index.html'; +chrome.setRootTemplate(template); +const dom = document.getElementById('react-apm-root'); + +core.applications.mountApp('demo', dom); + // tag_cloud/hacks/plugin.js // example init of updated plugin that consumes new interface From 329cdb11ecc15a0bc9ee4d582dbd7fef847aac55 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sun, 24 Feb 2019 14:30:33 -0500 Subject: [PATCH 13/46] extending an app --- src/core/MIGRATION.md | 82 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 38755b8eeac22..95bf2cb4f2982 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -467,7 +467,87 @@ How you accomplish these things varies wildly depending on the plugin's current Every plugin will add their global plugin definition via a `hack` uiExport, which will ensure that the plugin definition is always loaded for all applications. This is inline with how the plugin service works in the new platform. -Plugins that "own" a uiExport will move +##### Extending an application + +Let's take a look at a simple plugin that registers functionality to be used in an application. This is done by configuring a uiExport and accessing a registry through a `ui/registry` webpack alias: + +```js +// demo/index.js +{ + uiExports: { + foo: 'plugins/demo/some_file' + } +} + +// demo/public/some_file.js +import chrome from 'ui/chrome'; +import { FooRegistryProvider } from 'ui/registry/foo'; + +FooRegistryProvider.register(() => { + return { + url: chrome.getBasePath() + '/demo_foo' + }; +}); +``` + +To update this plugin, we'll create a plugin definition in a hack uiExport, and we'll move the registration logic there where we'll create some shims into the legacy world. + +```ts +// demo/index.js +{ + uiExports: { + hacks: [ + 'plugins/demo/hacks/shim_plugin' + ] + } +} + +// demo/public/plugin.js +import { CoreStart } from '../../core/public'; +import { FooPluginStart } from '../foo/public'; + +interface DemoStartDependencies { + foo: FooPluginStart +} + +export class Plugin { + public start(core: CoreStart, dependencies: DemoStartDependencies) { + const { chrome } = core; + + dependencies.foo.registerFoo(() => { + return { + url: chrome.getBasePath() + '/demo_foo' + }; + }); + } +} + +// demo/public/hacks/shim_plugin.js +import chrome from 'ui/chrome'; +import { FooRegistryProvider } from 'ui/registry/foo'; +import { Plugin } from '../plugin'; + +const core = { + chrome: { + getBasePath() { + return chrome.getBasePath(); + } + } +}; +const dependencies = { + foo: { + registerFoo(fn) { + FooRegistryProvider.register(fn); + } + } +}; + +new Plugin().start(core, dependencies); +``` + +The `shim_plugin.js` file is take on the role of the plugin service in the new platform. It wires up the plugin definition with the dependencies that plugin has on core (i.e. `chrome`) and other plugins (i.e. `foo`). All of the webpack alias imports needed by this plugin have been moved into the shim, and the `plugin.js` code is pristine. + + From 2e6ff96e33f15ef15643ee1fd6a51432f06ca27e Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sun, 24 Feb 2019 16:09:43 -0500 Subject: [PATCH 14/46] creating a client-side extension point --- src/core/MIGRATION.md | 86 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 95bf2cb4f2982..6d7453263721f 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -547,7 +547,93 @@ new Plugin().start(core, dependencies); The `shim_plugin.js` file is take on the role of the plugin service in the new platform. It wires up the plugin definition with the dependencies that plugin has on core (i.e. `chrome`) and other plugins (i.e. `foo`). All of the webpack alias imports needed by this plugin have been moved into the shim, and the `plugin.js` code is pristine. +##### Creating an extension +Legacy plugins today extend applications by adding functionality through a registry in a uiExport. In the previous example, you saw how to shim this relationship from the extending side into the new plugin definition. Now, let's see how to shim the relavant uiExport registry from the side of the plugin that "owns" it. + +We need to update the registry to expose access to state as an observable. In most cases, this will only affect the implementation details of the owning plugin. This is how state should be shared between plugins in the new platform, but more importantly it is necessary now to move away from the uiExports extension while the order of legacy plugin execution is not determined by a dependency graph. + +In order to support dependent legacy plugins that have not yet been updated, we continue to initiate the uiExport in the app entry file. Once all downstream plugins have been updated to access the registry in a shimmed plugin definition, the `uiExports/` import statement from the app entry file can be removed. + +```ts +// foo/public/plugin.ts +import { ReplaySubject } from 'rxjs'; + +export type FooPluginStart = ReturnType; + +export class Plugin { + public constructor() { + this.fooRegistry = []; + this.foos$ = new ReplaySubject(1); + } + + public start() { + return { + foos$: this.foos$.asObservable(), + registerFoo(fn) { + this.fooRegistry.push(fn); + this.fooSubject.next([ ...this.fooRegistry ]); + } + }; + } +} + + +// foo/hacks/shim_plugin.ts +import { startContracts } from 'ui/legacy/plugins'; +import { Plugin } from '../plugin'; + +const plugin = new Plugin(); +const start = plugin.start(); +startContracts.set('foo', start); + +require('ui/registry/foo').__temporaryShim__(start); + + +// ui/public/registry/foo.ts +import { ReplaySubject } from 'rxjs'; +import { FooPluginStart } from '../../../core_plugins/foo/public/plugin'; + +// legacy plugin order is not guaranteed, so we store a buffer of registry +// calls and then empty them out when the owning plugin shims this module with +// proper state +let temporaryRegistry = []; +let start; +export function __temporaryShim__(fooStart: FooPluginStart) { + if (start) { + throw new Error('Foo registry already shimmed'); + } + start = fooStart; + temporaryRegistry.forEach(fn => start.registerFoo(fn)); + temporaryRegistry = undefined; +} + +export const FooRegistryProvider = { + register(fn) { + if (start) { + start.registerFoo(fn); + } else { + temporaryRegistry.push(fn); + } + }, + getAll() { + if (start) { + return start.foos$; + } else { + throw new Error('Foo registry not yet shimmed'); + } + } +}; + + +// foo/public/index.js +import 'uiExports/foo'; // to continue support for non-updated plugins + +import { FooRegistryProvider } from 'ui/registry/foo'; +FooRegistryProvider.getAll().do(foos => { + // do something with array foos +}); +``` From 3477fa936310216dfbdc220d0a6706a9eff89481 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sun, 24 Feb 2019 16:10:52 -0500 Subject: [PATCH 15/46] remove unnecessary ReplaySubject in registry shim --- src/core/MIGRATION.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 6d7453263721f..f6af2aa940eec 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -591,7 +591,6 @@ require('ui/registry/foo').__temporaryShim__(start); // ui/public/registry/foo.ts -import { ReplaySubject } from 'rxjs'; import { FooPluginStart } from '../../../core_plugins/foo/public/plugin'; // legacy plugin order is not guaranteed, so we store a buffer of registry From 8e5305bc58d20fe3df2b3336c5dbda8a1c36230c Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sun, 24 Feb 2019 19:56:38 -0500 Subject: [PATCH 16/46] cleanup extension example --- src/core/MIGRATION.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index f6af2aa940eec..4e552233332f9 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -561,6 +561,8 @@ import { ReplaySubject } from 'rxjs'; export type FooPluginStart = ReturnType; +export type FooFn = () => void; + export class Plugin { public constructor() { this.fooRegistry = []; @@ -576,22 +578,24 @@ export class Plugin { } }; } + + public stop() { + this.foos$.complete(); + } } // foo/hacks/shim_plugin.ts -import { startContracts } from 'ui/legacy/plugins'; import { Plugin } from '../plugin'; const plugin = new Plugin(); const start = plugin.start(); -startContracts.set('foo', start); require('ui/registry/foo').__temporaryShim__(start); // ui/public/registry/foo.ts -import { FooPluginStart } from '../../../core_plugins/foo/public/plugin'; +import { FooPluginStart, FooFn } from '../../../core_plugins/foo/public/plugin'; // legacy plugin order is not guaranteed, so we store a buffer of registry // calls and then empty them out when the owning plugin shims this module with @@ -608,7 +612,7 @@ export function __temporaryShim__(fooStart: FooPluginStart) { } export const FooRegistryProvider = { - register(fn) { + register(fn: FooFn) { if (start) { start.registerFoo(fn); } else { @@ -629,7 +633,7 @@ export const FooRegistryProvider = { import 'uiExports/foo'; // to continue support for non-updated plugins import { FooRegistryProvider } from 'ui/registry/foo'; -FooRegistryProvider.getAll().do(foos => { +FooRegistryProvider.getAll().subscribe(foos => { // do something with array foos }); ``` From 9481898e24f71451ea290e37bffcb9734a075b29 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 26 Feb 2019 09:32:10 -0500 Subject: [PATCH 17/46] fix typo --- src/core/MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 4e552233332f9..2a0d9deb26b9a 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -269,7 +269,7 @@ If your plugin is registering some sort of global behavior that technically cros If your plugin is using angular only in the context of its own application, then removing angular is likely not a definitive requirement for moving to the new platform. In this case, you will need to refactor your plugin to initialize an entirely standalone angular module that serves your application. You will need to create custom wrappers for any of the angular services you previously relied on (including those through `Private()`) inside your own plugin. -At this poing, keeping angular around is not the recommended approach. If you feel you must do it, then talk to the platform team directly and we can help you craft a plan. +At this point, keeping angular around is not the recommended approach. If you feel you must do it, then talk to the platform team directly and we can help you craft a plan. We recommend that _all_ plugins treat moving away from angular as a top-most priority if they haven't done so already. From cf8b02ea9aa3c50509f74d3e4592d4956870589c Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 26 Feb 2019 12:04:17 -0500 Subject: [PATCH 18/46] react app example --- src/core/MIGRATION.md | 163 ++++++++++++++++++++++++++++-------------- 1 file changed, 108 insertions(+), 55 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 2a0d9deb26b9a..7098185e8e7ea 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -638,7 +638,115 @@ FooRegistryProvider.getAll().subscribe(foos => { }); ``` +##### Plugin with generic application +It is difficult to provide concrete guidance for migrating an application because most applications have unique architectures and bootstrapping logic. + +In the new plugin system, a plugin registers an application via the application service by providing an asyncronous `mount` function that core invokes when a user attempts to load the app. In this mounting function, a plugin would use an async import statement to load the application code so it wasn't loaded by the browser until it was first navigated to. + +The basic interface would be something similar to: + +```ts +async function mount({ dom }) { + const { bootstrapApp } = await import('/application'); + + const unmount = bootstrapApp({ dom }); + + // returns a function that cleans up after app when navigating away + return unmount; +} + +// application.ts +export function bootstrapApp({ dom }) { + // all of the application-specific setup logic + // application renders into the given dom element +} +``` + +This pattern provides flexibility for applications to have bootstrap logic and technologies that are not prescribed by or coupled to core itself. + +Applications in legacy plugins are instead resolved on the server-side and then get served to the client via application-specific entry files. Migrating applications involves adopting the above pattern for defining mounting logic in the global plugin definition hack, and then updating the legacy app entry file to behave like the new platform core will by invoking the mounting logic. + +As before, shims will need to be created for legacy integrations with core and other plugins, and all webpack alias-based imports will need to move to those shims. + +**React application:** + +Let's look at an example for a react application. + +```ts +// demo/public/plugin.js +export class Plugin { + start(core) { + const { I18nContext } = core.i18n; + core.applications.register('demo', async function mount(dom) { + const { bootstrapApp } = await import('../application'); + return bootstrapApp({ dom, I18nContext }); + }); + } +} + + +// demo/public/application.js +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router-dom'; +import ReactDOM from 'react-dom'; + +import { configureStore } from './store'; +import { Main } from './components/Main'; + +import './style/demo_custom_styles.css'; + +export function bootstrapApp({ dom, I18nContext }) { + const store = configureStore(); + + ReactDOM.render( + + + +
+ + + , + dom + ); + + return function destroyApp() { + ReactDOM.unmountComponentAtNode(dom); + } +} + + +// demo/public/hacks/shim_plugin.js +import { I18nContext } from 'ui/i18n'; +import { Plugin } from '../plugin'; + +const core = { + i18n: { + I18nContext + } +}; + +new Plugin().start(core); + + +// demo/public/index.js +import { core } from 'ui/core'; +import { uiModules } from 'ui/modules'; // eslint-disable-line no-unused-vars +import 'ui/autoload/styles'; +import 'ui/autoload/all'; + +import template from './templates/index.html'; +chrome.setRootTemplate(template); +const dom = document.getElementById('react-apm-root'); + +core.applications.mountApp('demo', dom); +``` + + + + +# Random temporary idea thrashing below * Plugin without app * Plugin with angular app @@ -715,61 +823,6 @@ uiModules.get('kibana').run(showAppRedirectNotification); -// example of plugin file for upgraded react plugin -// plugin -export class Plugin { - start(core, dependencies) { - const { I18nContext } = core.i18n; - core.applications.register('demo', async (dom) => { - const { mount } = await import('../application'); - return mount({ dom, I18nContext }); - }); - } -} - -// example of app entry file for upgraded react plugin -// application -import React from 'react'; -import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; -import ReactDOM from 'react-dom'; - -import { configureStore } from './store'; -import { Main } from './components/Main'; - -import './style/global_overrides.css'; - -export function mount({ dom, I18nContext }) { - const store = configureStore(); - - ReactDOM.render( - - - -
- - - , - dom - ); - - return function unmount() { - ReactDOM.unmountComponentAtNode(dom); - } -} - - -// entry -import { core } from 'ui/core'; -import { uiModules } from 'ui/modules'; // eslint-disable-line no-unused-vars -import 'ui/autoload/styles'; -import 'ui/autoload/all'; - -import template from './templates/index.html'; -chrome.setRootTemplate(template); -const dom = document.getElementById('react-apm-root'); - -core.applications.mountApp('demo', dom); // tag_cloud/hacks/plugin.js From 8b9dfd0abae3e933bbd0543e943a20431de7b6e5 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 26 Feb 2019 12:19:48 -0500 Subject: [PATCH 19/46] dom -> domElement --- src/core/MIGRATION.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 7098185e8e7ea..22adfc5bfce25 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -647,17 +647,17 @@ In the new plugin system, a plugin registers an application via the application The basic interface would be something similar to: ```ts -async function mount({ dom }) { +async function mount({ domElement }) { const { bootstrapApp } = await import('/application'); - const unmount = bootstrapApp({ dom }); + const unmount = bootstrapApp({ domElement }); // returns a function that cleans up after app when navigating away return unmount; } // application.ts -export function bootstrapApp({ dom }) { +export function bootstrapApp({ domElement }) { // all of the application-specific setup logic // application renders into the given dom element } @@ -678,9 +678,9 @@ Let's look at an example for a react application. export class Plugin { start(core) { const { I18nContext } = core.i18n; - core.applications.register('demo', async function mount(dom) { + core.applications.register('demo', async function mount(domElement) { const { bootstrapApp } = await import('../application'); - return bootstrapApp({ dom, I18nContext }); + return bootstrapApp({ domElement, I18nContext }); }); } } @@ -697,7 +697,7 @@ import { Main } from './components/Main'; import './style/demo_custom_styles.css'; -export function bootstrapApp({ dom, I18nContext }) { +export function bootstrapApp({ domElement, I18nContext }) { const store = configureStore(); ReactDOM.render( @@ -708,11 +708,11 @@ export function bootstrapApp({ dom, I18nContext }) { , - dom + domElement ); return function destroyApp() { - ReactDOM.unmountComponentAtNode(dom); + ReactDOM.unmountComponentAtNode(domElement); } } @@ -738,9 +738,9 @@ import 'ui/autoload/all'; import template from './templates/index.html'; chrome.setRootTemplate(template); -const dom = document.getElementById('react-apm-root'); +const domElement = document.getElementById('react-apm-root'); -core.applications.mountApp('demo', dom); +core.applications.mountApp('demo', domElement); ``` @@ -781,11 +781,11 @@ export class Plugin { } start(core, dependencies) { - core.applications.registerApp('visualize', (dom) => { + core.applications.registerApp('visualize', (domElement) => { this.legacyHackApp(); import('../application').then(({ bootstrapApp }) => { - const app = bootstrapApp(dom); + const app = bootstrapApp(domElement); }); return app.start(); From 7d1283e510e3d47943846e9c9de30723a54a29b7 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 26 Feb 2019 12:34:15 -0500 Subject: [PATCH 20/46] remove unnecessary imports from react app example --- src/core/MIGRATION.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 22adfc5bfce25..29e949b3f3a75 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -732,9 +732,7 @@ new Plugin().start(core); // demo/public/index.js import { core } from 'ui/core'; -import { uiModules } from 'ui/modules'; // eslint-disable-line no-unused-vars import 'ui/autoload/styles'; -import 'ui/autoload/all'; import template from './templates/index.html'; chrome.setRootTemplate(template); From e895edc27e2803036497c7860220accafb0cb921 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 26 Feb 2019 15:36:43 -0500 Subject: [PATCH 21/46] fix up react rendering example --- src/core/MIGRATION.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 29e949b3f3a75..762cd077fbc29 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -718,10 +718,12 @@ export function bootstrapApp({ domElement, I18nContext }) { // demo/public/hacks/shim_plugin.js +import { coreStart } from 'ui/core'; import { I18nContext } from 'ui/i18n'; import { Plugin } from '../plugin'; const core = { + ...coreStart, i18n: { I18nContext } @@ -731,14 +733,16 @@ new Plugin().start(core); // demo/public/index.js -import { core } from 'ui/core'; +import { coreInternals } from 'ui/core'; import 'ui/autoload/styles'; import template from './templates/index.html'; chrome.setRootTemplate(template); -const domElement = document.getElementById('react-apm-root'); -core.applications.mountApp('demo', domElement); +chrome.setRootController(() => { + const domElement = document.getElementById('custom-app-root'); + coreInternals.applications.mountApp('demo', domElement); +}); ``` From 2b4524c691636bc31238bfe6f876885843f43ee9 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Tue, 26 Feb 2019 15:43:33 -0500 Subject: [PATCH 22/46] remove some cruft from react render example --- src/core/MIGRATION.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 762cd077fbc29..5aa7db9cc6844 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -736,12 +736,8 @@ new Plugin().start(core); import { coreInternals } from 'ui/core'; import 'ui/autoload/styles'; -import template from './templates/index.html'; -chrome.setRootTemplate(template); - -chrome.setRootController(() => { - const domElement = document.getElementById('custom-app-root'); - coreInternals.applications.mountApp('demo', domElement); +chrome.setRootController(function ($element) { + coreInternals.applications.mountApp('demo', $element[0]); }); ``` From 4d846984730911653cd91befa137e60fb500d57d Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Wed, 27 Feb 2019 12:10:06 -0500 Subject: [PATCH 23/46] random thoughts around angular apps --- src/core/MIGRATION.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 5aa7db9cc6844..80185fd78ba1f 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -780,8 +780,6 @@ export class Plugin { start(core, dependencies) { core.applications.registerApp('visualize', (domElement) => { - this.legacyHackApp(); - import('../application').then(({ bootstrapApp }) => { const app = bootstrapApp(domElement); }); @@ -810,6 +808,25 @@ import './visualize'; import 'ui/vislib'; import { showAppRedirectNotification } from 'ui/notify'; +import { application } from 'ui/core'; + +chrome.setRootController(class { + constructor($element) { + core.applications.mountApp('demo', $element[0]); + } +}); + +import template from './templates/index.html'; +chrome.setRootTemplate(template); +initTimepicker().then(() => { + +}) + + + + + + routes.enable(); routes @@ -819,6 +836,9 @@ routes uiModules.get('kibana').run(showAppRedirectNotification); +bootstrapAngularChrome(); + + From e1d83468e33b613f9cb2af70b0cabf9b9e2783d8 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Wed, 27 Feb 2019 12:14:43 -0500 Subject: [PATCH 24/46] wrap up react app --- src/core/MIGRATION.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 80185fd78ba1f..db09019e466de 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -741,6 +741,7 @@ chrome.setRootController(function ($element) { }); ``` +The plugin and application bundles do not use webpack aliases for imports. Stateful services are passed around as function arguments. The plugin definition shim wires up the necessary bits of the core start contract, and the legacy app entry file shims the app-specific behaviors that ultimately will move into core (e.g. mounting the application) or will go away entirely (ui/autoload/styles). From 479e8bbfcd81098387ec9f8db733be2f01dddaad Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Wed, 27 Feb 2019 12:27:17 -0500 Subject: [PATCH 25/46] removing outdated temporary thrashing --- src/core/MIGRATION.md | 166 +----------------------------------------- 1 file changed, 4 insertions(+), 162 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index db09019e466de..ffb402195cb9a 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -743,59 +743,18 @@ chrome.setRootController(function ($element) { The plugin and application bundles do not use webpack aliases for imports. Stateful services are passed around as function arguments. The plugin definition shim wires up the necessary bits of the core start contract, and the legacy app entry file shims the app-specific behaviors that ultimately will move into core (e.g. mounting the application) or will go away entirely (ui/autoload/styles). +**Angular application:** +Angular applications must be handled a little differently since angular is still temporarily a part of core, and you cannot easily embed isolated angular applications within one another at runtime. Angular will be moved out of core and into individual plugins in 7.x, but in the meantime plugins can adopt at least some of the new plugin system conventions in their legacy angular applications. -# Random temporary idea thrashing below - -* Plugin without app -* Plugin with angular app -* Plugin with react app - -Client-side legacy plugin code is where things get weird, but the approach is largely the same - in the public entry file of the plugin, we separate the legacy integrations with the new plugin definition using a temporary "shim". - -As before, let's start with an example legacy client-side plugin. This example integrates with core, consumes functionality from another plugin, and exposes functionality for other plugins to consume via `uiExports`. This would be the rough shape of the code that would originate in the entry file, which would be either `index.ts` or `.ts`: - -```js -// visualize/hacks/plugin.js -// example init of updated plugin that preserves legacy API and introduces new one -import { startContracts } from 'ui/legacy/plugins'; -import { Plugin } from '../plugin'; -const core = {}; -const dependencies = {} -const plugin = new Plugin(); -const start = plugin.start(core, dependencies); -startContracts.set('visualize', start); - -require('ui/registry/vis_types').__shimYourStuff__(plugin.visTypes$); -require('uiExports/visTypes'); -// visualize/plugin.js -// example of upgraded plugin definition -export class Plugin { - constructor() { - this.visTypes$ = new ReplaySubject(1); - } - - start(core, dependencies) { - core.applications.registerApp('visualize', (domElement) => { - import('../application').then(({ bootstrapApp }) => { - const app = bootstrapApp(domElement); - }); - - return app.start(); - }); +# Random temporary idea thrashing below - return { - registerVisType(type) { - this.visTypes$.push(type); - } - }; - } -} +```js // visualize/index.js // example of app entry file for upgraded angular plugin import chrome from 'ui/chrome'; @@ -824,10 +783,6 @@ initTimepicker().then(() => { }) - - - - routes.enable(); routes @@ -838,119 +793,6 @@ routes uiModules.get('kibana').run(showAppRedirectNotification); bootstrapAngularChrome(); - - - - - - -// tag_cloud/hacks/plugin.js -// example init of updated plugin that consumes new interface -import { startContracts } from 'ui/legacy/plugins'; -import { Plugin } from '../plugin'; - -const core = {}; -const dependencies = { - ...startContracts -} - -const plugin = new Plugin(); -plugin.start(core, dependencies); - - - - - - - - - - - -// visualize/public/index.js -import { coreStart } from 'ui/core'; -import { foo } from 'ui/plugins'; - -import { visualize } from 'ui/legacy_plugins'; - -import { getBar } from 'plugins/bar'; - -const core = { - chrome: coreStart.chrome -}; - -const dependencies = { - bar: { - getBar - } -}; - -export class Plugin { - constructor() { - this.visTypes$ = new ReplaySubject(1); - } - start(core, dependencies) { - //require('ui/registry/vis_types').__shimYourStuff__(this.visTypes$); - require('uiExports/visTypes'); - - return { - visTypes$ = this.visTypes$.asObservable(); - myVis() { - - } - } - } -} - -export startContract = new Plugin(); - -//const start = new Plugin().start(core); - -export function myVis() { - return start.myVis(); -} - - - - -// my_custom_visualization/index.js -{ - uiExports: { - vis_type: 'plugins/my_custom_visulaization/some_file' - } -} -// my_custom_visualization/public/some_file.js -import { VisTypesProvider } from 'ui/registry/vis_types'; -VisTypesProvider.register(($http) => { - return { type: 'mycustomvis' }; -}); -``` - -Expressed in the shape of a new plugin: - -```ts -import { CoreStart } from '../../core/public'; -import { FooPluginStart } from '../foo/public'; - -interface DemoStartDependencies { - foo: FooPluginStart -} - -export type DemoPluginStart = ReturnType; - -export class Plugin { - public start(core: CoreStart, dependencies: DemoStartDependencies) { - core.router.when('/demo-foo', async (request) => { - - }); - - return { - registerExtension() { - - } - }; - } -} ``` From 8654982268afd09c93ec79f64ae79d70a7d0665a Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Wed, 27 Feb 2019 14:08:03 -0500 Subject: [PATCH 26/46] hacking around on angular app example --- src/core/MIGRATION.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index ffb402195cb9a..fb45078c6039e 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -745,8 +745,32 @@ The plugin and application bundles do not use webpack aliases for imports. State **Angular application:** -Angular applications must be handled a little differently since angular is still temporarily a part of core, and you cannot easily embed isolated angular applications within one another at runtime. Angular will be moved out of core and into individual plugins in 7.x, but in the meantime plugins can adopt at least some of the new plugin system conventions in their legacy angular applications. +WIP +Angular applications must be handled a little differently since angular is still temporarily a part of core, and you cannot easily embed isolated angular applications within one another at runtime. Angular will be moved out of core and into individual plugins in 7.x, but in the meantime the angular application logic should continue to be bootstrapped through the legacy app entry file. + +The best possible outcome is for applications to remove angular entirely in favor of React, but if the intention is to preserve the angular app post-migration, then the focus today should be on removing dependencies on external services (provided by core or other plugins) that are injected through the angular dependency injection mechanism. + +In the future, services provided by core and other plugins will not be available automatically via the angular dependency injection system. To prepare for that inevitability, angular applications should be updated to define those services themselves. For now, this can be done through shims. + +Let's consider the following example that relies on the core `chrome` service accessed through the angular dependency injection mechanism. + +```js +// demo/public/index.js +import routes from 'ui/routes'; + +import './directives'; +import './services'; + +routes.enable(); +routes.when('/demo', { + controller(chrome) { + this.basePath = chrome.getBasePath(); + } +}); +``` + +The demo application does not "own" `chrome`, so it won't exist automatically in the future. To continue using it, the demo application will need to configure it From 66039b77cbe57618772001edbc8e709403d9c8c5 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 9 Mar 2019 16:14:25 -0500 Subject: [PATCH 27/46] overhaul structure, flesh out server-side --- src/core/MIGRATION.md | 368 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 363 insertions(+), 5 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index fb45078c6039e..38eb5398cbfa2 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -5,13 +5,22 @@ * Services * Integrating with other plugins * Challenges to overcome with legacy plugins -* Plan of action - * TypeScript - * De-angular - * Architectural changes with legacy "shim" + * Plan of action +* Server-side plan of action + * De-couple from hapi.js server and request objects + * Introduce new plugin definition shim * Switch to new platform services - * Migrate to the new platform + * Migrate to the new plugin system +* Browser-side plan of action + * Decouple UI modules from angular.js + * Move UI modules into plugins + * Introduce new plugin definition shim + * Introduce application shim + * Switch to new platform services + * Migrate to the new plugin system * Frequently asked questions + * Is migrating a plugin an all-or-nothing thing? + * Do plugins need to be converted to TypeScript? * How is static code shared between plugins? * How is "common" code shared on both the client and server? * When does code go into a plugin, core, or packages? @@ -243,6 +252,355 @@ The key challenge to overcome with legacy browser-side plugins will be convertin ### Plan of action +In order to move a legacy plugin to the new plugin system, the challenges on the server and in the browser must be addressed. Fortunately, **the hardest problems can be solved in legacy plugins today** without consuming the new plugin system at all. + +The approach and level of effort varies significantly between server and browser plugins, but at a high level the approach is the same. + +First, decouple your plugin's business logic from the dependencies that are not exposed through the new platform, hapi.js and angular.js. Then introduce plugin definitions that more accurately reflect how plugins are defined in the new platform. Finally, replace the functionality you consume from core and other plugins with their new platform equivalents. + +Once those things are finished for any given plugin, it can offically be switched to the new plugin system. + +## Server-side plan of action + +Legacy server-side plugins access functionality from core and other plugins at runtime via function arguments, which is similar to how they must be architected to use the new plugin system. This greatly simplifies the plan of action for migrating server-side plugins. + +### De-couple from hapi.js server and request objects + +Most integrations with core and other plugins occur through the hapi.js `server` and `request` objects, and neither of these things are exposed through the new platform, so tackle this problem first. + +Fortunately, decoupling from these objects is relatively straightforward. + +The server object is introduced to your plugin in its legacy `init` function, so in that function you will "pick" the functionality you actually use from `server` and attach it to a new interface, which you will then pass in all the places you had previously been passing `server`. + +The `request` object is introduced to your plugin in every route handler, so at the root of every route handler, you will create a new interface by "picking" the request information (e.g. body, headers) and core and plugin capabilities from the `request` object that you actually use and pass that in all the places you previously were passing `request`. + +Any calls to mutate either the server or request objects (e.g. server.decorate()) will be moved toward the root of the legacy `init` function if they aren't already there. + +Let's take a look at an example legacy plugin definition that uses both `server` and `request`. + +```ts +// likely imported from another file +function search(server, request) { + const { elasticsearch } = server.plugins; + return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); +} + +export default (kibana) => { + return new kibana.Plugin({ + id: 'demo_plugin', + + init(server) { + server.route({ + path: '/api/demo_plugin/search', + method: 'POST', + async handler(request) { + search(server, request); // target acquired + } + }); + + server.expose('getDemoBar', () => { + return `Demo ${server.plugins.foo.getBar()}`; + }); + } + }); +} +``` + +This example legacy plugin uses hapi's `server` object directly inside of its `init` function, which is something we can address in a later step. What we need to address in this step is when we pass the raw `server` and `request` objects into our custom `search` function. + +Instead, we identify which functionality we actually need from those objects and craft custom new interfaces for them, taking care not to leak hapi.js implementation details into their design. + +```ts +import { ElasticsearchPlugin, Request } from '../elasticsearch'; +export interface ServerFacade { + plugins: { + elasticsearch: ElasticsearchPlugin + } +} +export interface RequestFacade extends Request { +} + +// likely imported from another file +function search(server: ServerFacade, request: RequestFacade) { + const { elasticsearch } = server.plugins; + return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); +} + +export default (kibana) => { + return new kibana.Plugin({ + id: 'demo_plugin', + + init(server) { + const serverFacade: ServerFacade = { + plugins: { + elasticsearch: server.plugins.elasticsearch + } + } + + server.route({ + path: '/api/demo_plugin/search', + method: 'POST', + async handler(request) { + const requestFacade: RequestFacade = { + headers: request.headers + }; + search(serverFacade, requestFacade); + } + }); + + server.expose('getDemoBar', () => { + return `Demo ${server.plugins.foo.getBar()}`; + }); + } + }); +} +``` + +This change might seem trivial, but its important for two reasons. + +First, the business logic built into `search` is now coupled to an object you created manually and have complete control over rather than hapi itself. This will allow us in a future step to replace the dependency on hapi without necessarily having to modify the business logic of the plugin. + +Second, it forced you to clearly define the dependencies you have on capabilities provided by core and by other plugins. This will help in a future step when you must replace those capabilities with services provided through the new platform. + +### Introduce new plugin definition shim + +While most plugin logic is now decoupled from hapi, the plugin definition itself still uses hapi to expose functionality for other plugins to consume and accesses functionality from both core and a different plugin. + +```ts +// index.ts + +export default (kibana) => { + return new kibana.Plugin({ + id: 'demo_plugin', + + init(server) { + const serverFacade: ServerFacade = { + plugins: { + elasticsearch: server.plugins.elasticsearch + } + } + + // HTTP functionality from core + server.route({ + path: '/api/demo_plugin/search', + method: 'POST', + async handler(request) { + const requestFacade: RequestFacade = { + headers: request.headers + }; + search(serverFacade, requestFacade); + } + }); + + // Exposing functionality for other plugins + server.expose('getDemoBar', () => { + return `Demo ${server.plugins.foo.getBar()}`; // Accessing functionality from another plugin + }); + } + }); +} +``` + +We now move this logic into a new plugin definition, which is based off of the conventions used in real new platform plugins. While the legacy plugin definition is in the root of the plugin, this new plugin definition will be under the plugin's `server/` directory since it is only the server-side plugin definition. + +```ts +// server/plugin.ts +import { ElasticsearchPlugin } from '../elasticsearch'; + +interface CoreSetup { + elasticsearch: ElasticsearchPlugin // note: we know elasticsearch will move to core +} + +interface FooSetup { + getBar(): string +} + +interface DependenciesSetup { + foo: FooSetup +} + +export type DemoPluginSetup = ReturnType; + +export class Plugin { + public setup(core: CoreSetup, dependencies: DependenciesSetup) { + const serverFacade: ServerFacade = { + plugins: { + elasticsearch: core.elasticsearch + } + } + + // HTTP functionality from core + core.http.route({ // note: we know routes will be created on core.http + path: '/api/demo_plugin/search', + method: 'POST', + async handler(request) { + const requestFacade: RequestFacade = { + headers: request.headers + }; + search(serverFacade, requestFacade); + } + }); + + // Exposing functionality for other plugins + return { + getDemoBar() { + return `Demo ${dependencies.foo.getBar()}`; // Accessing functionality from another plugin + } + }; + } +} +``` + +The legacy plugin definition is still the one that is being executed, so we now "shim" this new plugin definition into the legacy world by instantiating it and wiring it up inside of the legacy `init` function. + +```ts +// index.ts + +import { Plugin } from './server/plugin'; + +export default (kibana) => { + return new kibana.Plugin({ + id: 'demo_plugin', + + init(server) { + // core shim + const coreSetup = { + elasticsearch: server.plugins.elasticsearch, + http: { + route: server.route + } + }; + // plugins shim + const dependenciesSetup = { + foo: server.plugins.foo + }; + + const demoSetup = new Plugin().setup(coreSetup, dependenciesSetup); + + // continue to expose functionality to legacy plugins + server.expose('getDemoBar', demoSetup.getDemoBar); + } + }); +} +``` + +This introduces a layer between the legacy plugin system with hapi.js and the logic you want to move to the new plugin system. The functionality exposed through that layer is still provided from the legacy world and in some cases is still technically powered directly by hapi, but building this layer forced you to identify the remaining touch points into the legacy world and it provides you with control when you start migrating to new platform-backed services. + +### Switch to new platform services + +At this point, your legacy server-side plugin is described in the shape and conventions of the new plugin system, and all of the touch points with the legacy world and hapi.js have been isolated to the shims in the legacy plugin definition. + +Now the goal is to replace the legacy services backing your shims with services provided by the new platform instead. + +For the first time in this guide, your progress here is limited by the migration efforts within core and other plugins. + +As core capabilities are migrated to services in the new platform, they are made available as lifecycle contracts to the legacy `init` function through `server.newPlatform`. This allows you to adopt the new platform service APIs directly in your legacy plugin as they get rolled out. + +For the most part, care has been taken when migrating services to the new platform to preserve the existing APIs as much as possible, but there will be times when new APIs differ from the legacy equivalents. Start things off by having your core shim extend the equivalent new platform contract. + +```ts +// index.ts + +init(server) { + // core shim + const coreSetup = { + ...server.newPlatform.setup.core, + + elasticsearch: server.plugins.elasticsearch, + http: { + route: server.route + } + }; +} +``` + +If a legacy API differs from its new platform equivalent, some refactoring will be required. The best outcome comes from updating the plugin code to use the new API, but if that's not practical now, you can also create a facade inside your new plugin definition that is shaped like the legacy API but powered by the new API. Once either of these things is done, that override can be removed from the shim. + +Eventually, all overrides will be removed and your `coreSetup` shim is entirely powered by `server.newPlatform.setup.core`. + +```ts +init(server) { + // core shim + const coreSetup = { + ...server.newPlatform.setup.core + }; +} +``` + +At this point, your legacy server-side plugin's logic is no longer coupled to the legacy core. + +A similar approach can be taken for your plugins shim. First, update your plugin shim in `init` to extend `server.newPlatform.setup.plugins`. + +```ts +init(server) { + // plugins shim + const dependenciesSetup = { + ...server.newPlatform.setup.plugins, + foo: server.plugins.foo + }; +} +``` + +As the plugins you depend on are migrated to the new platform, their contract will be exposed through `server.newPlatform`, so the legacy override should be removed. Like in core, plugins should take care to preserve their existing APIs to make this step as seamless as possible. + +It is much easier to reliably make breaking changes to plugin APIs in the new platform than it is in the legacy world, so if you're planning a big change, consider doing it after your dependent plugins have migrated rather than as part of your own migration. + +Eventually, all overrides will be removed and your `dependenciesSetup` shim is entirely powered by `server.newPlatform.setup.plugins`. + +```ts +init(server) { + // plugins shim + const dependenciesSetup = { + ...server.newPlatform.setup.plugins + }; +} +``` + +At this point, your legacy server-side plugin's logic is no longer coupled to legacy plugins. + +### Migrate to the new plugin system + +With both shims converted, you are now ready to complete your migration to the new platform. + +Details to come... + + +## Browser-side plan of action + +### Decouple UI modules from angular.js +### Move UI modules into plugins +### Introduce new plugin definition shim +### Introduce application shim +### Switch to new platform services +### Migrate to the new plugin system + +With all of your shims converted, you are now ready to complete your migration to the new platform. + +Details to come... + + + + + + + + + + + + + + + + + + +# Old stuff below this line. + +Most of the stuff below is still relevant, but I'm mid-overhaul of the structure of this document and the content below is from a prior draft. + +### Plan of action + In order to move a legacy plugin to the new plugin system, the challenges on the server and in the browser must be addressed. Fortunately, **the hardest problems can be solved in legacy plugins today** without consuming the new platform at all. At a high level, the bulk of the migration work can be broken down into two phases. From e6fbb30919119dd4c049e3a7cebec52024cc1361 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 9 Mar 2019 20:17:08 -0500 Subject: [PATCH 28/46] intro to browser --- src/core/MIGRATION.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 38eb5398cbfa2..ae98937a025c5 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -567,6 +567,10 @@ Details to come... ## Browser-side plan of action +It is generally a much greater challenge preparing legacy browser-side code for the new platform than it is server-side, and as such there are a few more steps. In general, the level of effort here is proportional to the extent to which a plugin is dependent on angular.js. + +To complicate matters further, a significant amount of the business logic in Kibana's client-side code exists inside the `ui/public` directory (aka ui modules), and all of that must be migrated as well. Unlike the server-side code where the order in which you migrated plugins was not particularly important, it's important that UI modules be addressed as soon as possible. + ### Decouple UI modules from angular.js ### Move UI modules into plugins ### Introduce new plugin definition shim From e720b27d92f421a71c881d4416673106aecbc7af Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 9 Mar 2019 20:56:46 -0500 Subject: [PATCH 29/46] pseudo code for loading np browser shims --- src/core/public/core_system.ts | 4 ++-- src/core/public/legacy/legacy_service.ts | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index ea934815fd448..0e96e659103d4 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -100,7 +100,7 @@ export class CoreSystem { }); } - public start() { + public async start() { try { // ensure the rootDomElement is empty this.rootDomElement.textContent = ''; @@ -125,7 +125,7 @@ export class CoreSystem { notifications, }); - this.legacyPlatform.start({ + await this.legacyPlatform.start({ i18n, injectedMetadata, fatalErrors, diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 6ab8b912bfcef..1dae03f70509d 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -54,7 +54,7 @@ export interface LegacyPlatformParams { export class LegacyPlatformService { constructor(private readonly params: LegacyPlatformParams) {} - public start(deps: Deps) { + public async start(deps: Deps) { const { i18n, injectedMetadata, @@ -86,6 +86,28 @@ export class LegacyPlatformService { // the bootstrap module can modify the environment a bit first const bootstrapModule = this.loadBootstrapModule(); + // emulates new platform-like loading cycle + // can replace hacks and chrome uiExports + + // shim plugin instantiation + // @ts-ignore + const plugins = injectedMetadata.getSortedPluginNames().map((name: string) => { + const { shim } = require(`plugins/${name}/np_plugin`); + return { name, plugin: shim() }; + }); + + // shim setup task + for (const { name, plugin } of plugins) { + const { shim } = require(`plugins/${name}/np_plugin_setup`); + await shim(plugin); + } + + // shim start task + for (const { name, plugin } of plugins) { + const { shim } = require(`plugins/${name}/np_plugin_start`); + await shim(plugin); + } + // require the files that will tie into the legacy platform this.params.requireLegacyFiles(); From f88c7b2e43d6f2f938c5ab05ddfa38e8f6a0687c Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 15 Mar 2019 13:59:08 -0400 Subject: [PATCH 30/46] start -> setup --- src/core/MIGRATION.md | 128 +++++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index ae98937a025c5..5694a5b827b36 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -81,14 +81,14 @@ export function plugin(initializerContext: PluginInitializerContext) { **[3] `public/plugin.ts`** is the client-side plugin definition itself. Technically speaking it does not need to be a class or even a separate file from the entry point, but _all plugins at Elastic_ should be consistent in this way. ```ts -import { PluginInitializerContext, CoreStart, PluginStop } from '../../../core/public'; +import { PluginInitializerContext, CoreSetup, PluginStop } from '../../../core/public'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { } - public start(core: CoreStart) { - // called when plugin is started up, aka when Kibana is loaded + public setup(core: CoreSetup) { + // called when plugin is setting up } public stop(core: PluginStop) { @@ -111,14 +111,14 @@ export function plugin(initializerContext: PluginInitializerContext) { **[5] `server/plugin.ts`** is the server-side plugin definition. The _shape_ of this plugin is the same as it's client-side counter-part: ```ts -import { PluginInitializerContext, CoreStart, PluginStop } from '../../../core/server'; +import { PluginInitializerContext, CoreSetup, PluginStop } from '../../../core/server'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { } - public start(core: CoreStart) { - // called when plugin is started up during Kibana's startup sequence + public setup(core: CoreSetup) { + // called when plugin is setting up during Kibana's startup sequence } public stop(core: PluginStop) { @@ -131,38 +131,38 @@ The platform does not impose any technical restrictions on how the internals of ### Services -The various independent domains that make up `core` are represented by a series of services, and many of those services expose public interfaces that are provided to _all_ plugins via the first argument of their `start` and `stop` functions. The interface varies from service to service, but it is always accessed through this argument. +The various independent domains that make up `core` are represented by a series of services, and many of those services expose public interfaces that are provided to _all_ plugins via the first argument of their `setup` and `stop` functions. The interface varies from service to service, but it is always accessed through this argument. -For example, the core `UiSettings` service exposes a function `get` to all plugin `start` functions. To use this function to retrieve a specific UI setting, a plugin just accesses it off of the first argument: +For example, the core `UiSettings` service exposes a function `get` to all plugin `setup` functions. To use this function to retrieve a specific UI setting, a plugin just accesses it off of the first argument: ```ts -import { CoreStart } from '../../../core/public'; +import { CoreSetup } from '../../../core/public'; export class Plugin { - public start(core: CoreStart) { + public setup(core: CoreSetup) { core.uiSettings.get('courier:maxShardsBeforeCryTime'); } } ``` -Different service interfaces can and will be passed to `start` and `stop` because certain functionality makes sense in the context of a running plugin while other types of functionality may have restrictions or may only make sense in the context of a plugin that is stopping. +Different service interfaces can and will be passed to `setup` and `stop` because certain functionality makes sense in the context of a running plugin while other types of functionality may have restrictions or may only make sense in the context of a plugin that is stopping. For example, the `stop` function in the browser gets invoked as part of the `window.onbeforeunload` event, which means you can't necessarily execute asynchronous code here in a reliable way. For that reason, `core` likely wouldn't provide any asynchronous functions to plugin `stop` functions in the browser. ### Integrating with other plugins -Plugins can expose public interfaces for other plugins to consume. Like `core`, those interfaces are bound to `start` and/or `stop`. +Plugins can expose public interfaces for other plugins to consume. Like `core`, those interfaces are bound to `setup` and/or `stop`. -Anything returned from `start` or `stop` will act as the interface, and while not a technical requirement, all Elastic plugins should expose types for that interface as well. +Anything returned from `setup` or `stop` will act as the interface, and while not a technical requirement, all Elastic plugins should expose types for that interface as well. **foobar plugin.ts:** ```ts -export type FoobarPluginStart = ReturnType; +export type FoobarPluginSetup = ReturnType; export type FoobarPluginStop = ReturnType; export class Plugin { - public start() { + public setup() { return { getFoo() { return 'foo'; @@ -193,16 +193,16 @@ Unlike core, capabilities exposed by plugins are _not_ automatically injected in } ``` -With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of `start` and/or `stop`: +With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of `setup` and/or `stop`: **demo plugin.ts:** ```ts -import { CoreStart, PluginStop } from '../../../core/server'; -import { FoobarPluginStart, FoobarPluginStop } from '../../foobar/server'; +import { CoreSetup, PluginStop } from '../../../core/server'; +import { FoobarPluginSetup, FoobarPluginStop } from '../../foobar/server'; -interface DemoStartDependencies { - foobar: FoobarPluginStart +interface DemoSetupDependencies { + foobar: FoobarPluginSetup } interface DemoStopDependencies { @@ -210,7 +210,7 @@ interface DemoStopDependencies { } export class Plugin { - public start(core: CoreStart, dependencies: DemoStartDependencies) { + public setup(core: CoreSetup, dependencies: DemoSetupDependencies) { const { foobar } = dependencies; foobar.getFoo(); // 'foo' foobar.getBar(); // throws because getBar does not exist @@ -234,7 +234,7 @@ This means that there are unique sets of challenges for migrating to the new pla The general shape/architecture of legacy server-side code is similar to the new platform architecture in one important way: most legacy server-side plugins define an `init` function where the bulk of their business logic begins, and they access both "core" and "plugin-provided" functionality through the arguments given to `init`. Rarely does legacy server-side code share stateful services via import statements. -While not exactly the same, legacy plugin `init` functions behave similarly today as new platform `start` functions. There is no corresponding legacy concept of `stop`, however. +While not exactly the same, legacy plugin `init` functions behave similarly today as new platform `setup` functions. There is no corresponding legacy concept of `stop`, however. Despite their similarities, server-side plugins pose a formidable challenge: legacy core and plugin functionality is retrieved from either the hapi.js `server` or `request` god objects. Worse, these objects are often passed deeply throughout entire plugins, which directly couples business logic with hapi. And the worst of it all is, these objects are mutable at any time. @@ -671,17 +671,17 @@ export default (kibana) => { If we were to express this same set of capabilities in a shape that's more suitable to the new plugin system, it would look something like this: ```ts -import { CoreStart } from '../../core/server'; -import { FooPluginStart } from '../foo/server'; +import { CoreSetup } from '../../core/server'; +import { FooPluginSetup } from '../foo/server'; -interface DemoStartDependencies { - foo: FooPluginStart +interface DemoSetupDependencies { + foo: FooPluginSetup } -export type DemoPluginStart = ReturnType; +export type DemoPluginSetup = ReturnType; export class Plugin { - public start(core: CoreStart, dependencies: DemoStartDependencies) { + public setup(core: CoreSetup, dependencies: DemoSetupDependencies) { core.http.route({ path: '/api/demo_plugin/search', method: 'POST', @@ -722,7 +722,7 @@ export class Plugin { ##### Starting the plugin up -The new plugin definition uses `start` instead of `init`, and rather than getting the hapi server object as its only argument, it gets two arguments: the core services and a dependency on another plugin. +The new plugin definition uses `setup` instead of `init`, and rather than getting the hapi server object as its only argument, it gets two arguments: the core services and a dependency on another plugin. ```ts // before @@ -731,14 +731,14 @@ init(server) { } //after -public start(core: CoreStart, dependencies: DemoStartDependencies) { +public setup(core: CoreSetup, dependencies: DemoSetupDependencies) { // ... } ``` ##### Accessing core services -Rather than accessing "core" functions like HTTP routing directly on a hapi server object, the new plugin accesses core functionality through the top level services it exposes in the first argument to `start`. In the case of HTTP routing, it uses `core.http`. +Rather than accessing "core" functions like HTTP routing directly on a hapi server object, the new plugin accesses core functionality through the top level services it exposes in the first argument to `setup`. In the case of HTTP routing, it uses `core.http`. ```ts // before @@ -766,7 +766,7 @@ core.http.route({ Legacy plugins on the server might expose functionality or services to other plugins by invoking the `expose` function on the hapi `server` object. This can happen at any time throughout the runtime of Kibana which makes it less than reliable. -New plugins return the contract (if any) that they wish to make available to downstream plugins. This ensures the entirety of a plugin's start contract is available upon completion of its own `start` function. It also makes it much easier to provide type definitions for plugin contracts. +New plugins return the contract (if any) that they wish to make available to downstream plugins. This ensures the entirety of a plugin's start contract is available upon completion of its own `setup` function. It also makes it much easier to provide type definitions for plugin contracts. ```ts // before @@ -784,7 +784,7 @@ return { ##### Accessing plugin services -In server-side code of legacy plugins, you once again use the hapi `server` object to access functionality that was exposed by other plugins. In new plugins, you access the exposed functionality in a similar way but on the second argument to `start` that is dedicated only to injecting plugin capabilities. +In server-side code of legacy plugins, you once again use the hapi `server` object to access functionality that was exposed by other plugins. In new plugins, you access the exposed functionality in a similar way but on the second argument to `setup` that is dedicated only to injecting plugin capabilities. ```ts // before @@ -801,14 +801,14 @@ One other thing worth noting in this example is how a new plugin will consume st This is done through standard modules and relative imports. ```ts -// import CoreStart type from core server -import { CoreStart } from '../../core/server'; +// import CoreSetup type from core server +import { CoreSetup } from '../../core/server'; -// import FooPluginStart type from plugin foo -import { FooPluginStart } from '../foo/server'; +// import FooPluginSetup type from plugin foo +import { FooPluginSetup } from '../foo/server'; -// export DemoPluginStart type for downstream plugins, based on return value of start() -export type DemoPluginStart = ReturnType; +// export DemoPluginSetup type for downstream plugins, based on return value of setup() +export type DemoPluginSetup = ReturnType; ``` While these particular examples involve only types, the exact same pattern should be followed for those rare situations when a plugin exposes static functionality for plugins to consume. @@ -865,15 +865,15 @@ To update this plugin, we'll create a plugin definition in a hack uiExport, and } // demo/public/plugin.js -import { CoreStart } from '../../core/public'; -import { FooPluginStart } from '../foo/public'; +import { CoreSetup } from '../../core/public'; +import { FooPluginSetup } from '../foo/public'; -interface DemoStartDependencies { - foo: FooPluginStart +interface DemoSetupDependencies { + foo: FooPluginSetup } export class Plugin { - public start(core: CoreStart, dependencies: DemoStartDependencies) { + public setup(core: CoreSetup, dependencies: DemoSetupDependencies) { const { chrome } = core; dependencies.foo.registerFoo(() => { @@ -904,7 +904,7 @@ const dependencies = { } }; -new Plugin().start(core, dependencies); +new Plugin().setup(core, dependencies); ``` The `shim_plugin.js` file is take on the role of the plugin service in the new platform. It wires up the plugin definition with the dependencies that plugin has on core (i.e. `chrome`) and other plugins (i.e. `foo`). All of the webpack alias imports needed by this plugin have been moved into the shim, and the `plugin.js` code is pristine. @@ -921,7 +921,7 @@ In order to support dependent legacy plugins that have not yet been updated, we // foo/public/plugin.ts import { ReplaySubject } from 'rxjs'; -export type FooPluginStart = ReturnType; +export type FooPluginSetup = ReturnType; export type FooFn = () => void; @@ -931,7 +931,7 @@ export class Plugin { this.foos$ = new ReplaySubject(1); } - public start() { + public setup() { return { foos$: this.foos$.asObservable(), registerFoo(fn) { @@ -951,39 +951,39 @@ export class Plugin { import { Plugin } from '../plugin'; const plugin = new Plugin(); -const start = plugin.start(); +const setup = plugin.setup(); -require('ui/registry/foo').__temporaryShim__(start); +require('ui/registry/foo').__temporaryShim__(setup); // ui/public/registry/foo.ts -import { FooPluginStart, FooFn } from '../../../core_plugins/foo/public/plugin'; +import { FooPluginSetup, FooFn } from '../../../core_plugins/foo/public/plugin'; // legacy plugin order is not guaranteed, so we store a buffer of registry // calls and then empty them out when the owning plugin shims this module with // proper state let temporaryRegistry = []; -let start; -export function __temporaryShim__(fooStart: FooPluginStart) { - if (start) { +let setup; +export function __temporaryShim__(fooSetup: FooPluginSetup) { + if (setup) { throw new Error('Foo registry already shimmed'); } - start = fooStart; - temporaryRegistry.forEach(fn => start.registerFoo(fn)); + setup = fooSetup; + temporaryRegistry.forEach(fn => setup.registerFoo(fn)); temporaryRegistry = undefined; } export const FooRegistryProvider = { register(fn: FooFn) { - if (start) { - start.registerFoo(fn); + if (setup) { + setup.registerFoo(fn); } else { temporaryRegistry.push(fn); } }, getAll() { - if (start) { - return start.foos$; + if (setup) { + return setup.foos$; } else { throw new Error('Foo registry not yet shimmed'); } @@ -1038,7 +1038,7 @@ Let's look at an example for a react application. ```ts // demo/public/plugin.js export class Plugin { - start(core) { + setup(core) { const { I18nContext } = core.i18n; core.applications.register('demo', async function mount(domElement) { const { bootstrapApp } = await import('../application'); @@ -1080,18 +1080,18 @@ export function bootstrapApp({ domElement, I18nContext }) { // demo/public/hacks/shim_plugin.js -import { coreStart } from 'ui/core'; +import { coreSetup } from 'ui/core'; import { I18nContext } from 'ui/i18n'; import { Plugin } from '../plugin'; const core = { - ...coreStart, + ...coreSetup, i18n: { I18nContext } }; -new Plugin().start(core); +new Plugin().setup(core); // demo/public/index.js @@ -1103,7 +1103,7 @@ chrome.setRootController(function ($element) { }); ``` -The plugin and application bundles do not use webpack aliases for imports. Stateful services are passed around as function arguments. The plugin definition shim wires up the necessary bits of the core start contract, and the legacy app entry file shims the app-specific behaviors that ultimately will move into core (e.g. mounting the application) or will go away entirely (ui/autoload/styles). +The plugin and application bundles do not use webpack aliases for imports. Stateful services are passed around as function arguments. The plugin definition shim wires up the necessary bits of the core setup contract, and the legacy app entry file shims the app-specific behaviors that ultimately will move into core (e.g. mounting the application) or will go away entirely (ui/autoload/styles). **Angular application:** From 7b635c707072acd47d204d6577e916d6f13c411e Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 15 Mar 2019 14:40:37 -0400 Subject: [PATCH 31/46] new browser-side details --- src/core/MIGRATION.md | 51 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 5694a5b827b36..a5d57dce6b1ba 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -12,10 +12,9 @@ * Switch to new platform services * Migrate to the new plugin system * Browser-side plan of action - * Decouple UI modules from angular.js * Move UI modules into plugins - * Introduce new plugin definition shim - * Introduce application shim + * Provide plugin extension points decoupled from angular.js + * Move all webpack alias imports into apiExport entry files * Switch to new platform services * Migrate to the new plugin system * Frequently asked questions @@ -567,18 +566,54 @@ Details to come... ## Browser-side plan of action -It is generally a much greater challenge preparing legacy browser-side code for the new platform than it is server-side, and as such there are a few more steps. In general, the level of effort here is proportional to the extent to which a plugin is dependent on angular.js. +It is generally a much greater challenge preparing legacy browser-side code for the new platform than it is server-side, and as such there are a few more steps. The level of effort here is proportional to the extent to which a plugin is dependent on angular.js. To complicate matters further, a significant amount of the business logic in Kibana's client-side code exists inside the `ui/public` directory (aka ui modules), and all of that must be migrated as well. Unlike the server-side code where the order in which you migrated plugins was not particularly important, it's important that UI modules be addressed as soon as possible. -### Decouple UI modules from angular.js +Also unlike the server-side migration, we won't concern ourselves with creating shimmed plugin definitions that then get copied over to complete the migration. + ### Move UI modules into plugins -### Introduce new plugin definition shim -### Introduce application shim + +Everything inside of the `ui/public` directory is going to be dealt with in one of the following ways: + +* Deleted because it doesn't need to be used anymore +* Moved to or replaced by something in core that isn't coupled to angular +* Moved to or replaced by an extension point in a specific plugin that "owns" that functionality +* Copied into each plugin that depends on it and becomes an implementation detail there + +To rapidly define ownership and determine interdependencies, UI modules should move to the most appropriate plugins to own them. Modules that are considered "core" can remain in the ui directory as the platform team works to move them out. + +Concerns around ownership or duplication of a given module should be raised and resolved with the appropriate team so that the code is either duplicated to break the interdependency or a team agrees to "own" that extension point in one of their plugins and the module moves there. + +A great outcome is a module being deleted altogether because it isn't used or it was used so lightly that it was easy to refactor away. + +### Provide plugin extension points decoupled from angular.js + +There will be no global angular module in the new platform, which means none of the functionality provided by core will be coupled to angular. Since there is no global angular module shared by all applications, plugins providing extension points to be used by other plugins can not couple those extension points to angular either. + +All teams that own a plugin are strongly encouraged to remove angular entirely, but if nothing else they must provide non-angular-based extension points for plugins. + +One way to address this problem is to go through the code that is currently exposed to plugins and refactor away all of the touch points into angular.js. This might be the easiest option in some cases, but it might be hard in others. + +Another way to address this problem is to create an entirely new set of plugin APIs that are not dependendent on angular.js, and then update the implementation within the plugin to "merge" the angular and non-angular capabilities together. This is a good approach if preserving the existing angular API until we remove the old plugin system entirely is of critical importance. Generally speaking though, the removal of angular and introduction of a new set of public plugin APIs is a good reason to make a breaking change to the existing plugin capabilities. Make sure the PRs are tagged appropriate so we add these changes to our plugin changes blog post for each release. + +### Move all webpack alias imports into apiExport entry files + +Existing plugins import three things using webpack aliases today: services from ui/public (`ui/`), services from other plugins (`plugins/`), and uiExports themselves (`uiExports/`). These webpack aliases will not exist once we remove the legacy plugin system, so part of our migration effort is addressing all of the places where they are used today. + +In the new platform, dependencies from core and other plugins will be passed through lifecycle functions in the plugin definition itself. In a sense, they will be run from the "root" of the plugin. + +With the legacy plugin system, extensions of core and other plugins are handled through entry files defined as uiExport paths. In other words, when a plugin wants to serve an application (a core-owned thing), it defines a main entry file for the app via the `app` uiExport, and when a plugin wants to extend visTypes (a plugin-owned thing), they do so by specifying an entry file path for the `visType` uiExport. + +Each uiExport path is an entry file into one specific set of functionality provided by a client-side plugin. All webpack alias-based imports should be moved to these entry files, where they are appropriate. Moving a deeply nested webpack alias-based import in a plugin to one of the uiExport entry files might require some refactoring to ensure the dependency is now passed down to the appropriate place as function arguments instead of via import statements. + ### Switch to new platform services + + + ### Migrate to the new plugin system -With all of your shims converted, you are now ready to complete your migration to the new platform. +With all of your services converted, you are now ready to complete your migration to the new platform. Details to come... From b6ec9217657c2b48c02deb0737bee2716e956e9b Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 15 Mar 2019 14:48:37 -0400 Subject: [PATCH 32/46] ui switch services --- src/core/MIGRATION.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index a5d57dce6b1ba..f055922a14007 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -609,7 +609,11 @@ Each uiExport path is an entry file into one specific set of functionality provi ### Switch to new platform services +At this point, your plugin has one or more uiExport entry files that together contain all of the webpack alias-based import statements needed to run your plugin. Each one of these import statements is either a service that is or will be provided by core or a service provided by another plugin. +As new non-angular-based APIs are added, update your entry files to import the correct service API. The service APIs provided directly from the new platform can be imported through the `ui/new_platform` module for the duration of this migration. As new services are added, they will also be exposed there. This includes all core services as well as any APIs provided by real new platform plugins. + +Once all of the existing webpack alias-based imports in your plugin switch to `ui/new_platform`, it no longer depends directly on the legacy "core" features or other legacy plugins, so it is ready to officially migrate to the new platform. ### Migrate to the new plugin system From c02119b5ff0a9ceb20322dc2e5c066782ad71fd9 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 15 Mar 2019 14:49:27 -0400 Subject: [PATCH 33/46] remove old stuff --- src/core/MIGRATION.md | 603 ------------------------------------------ 1 file changed, 603 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index f055922a14007..e412b51dd1e73 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -621,606 +621,3 @@ With all of your services converted, you are now ready to complete your migratio Details to come... - - - - - - - - - - - - - - - - - -# Old stuff below this line. - -Most of the stuff below is still relevant, but I'm mid-overhaul of the structure of this document and the content below is from a prior draft. - -### Plan of action - -In order to move a legacy plugin to the new plugin system, the challenges on the server and in the browser must be addressed. Fortunately, **the hardest problems can be solved in legacy plugins today** without consuming the new platform at all. - -At a high level, the bulk of the migration work can be broken down into two phases. - -First, refactor the plugin's architecture to isolate the legacy behaviors mentioned in the "Challenges to overcome with legacy plugins" section above. In practice, this involves moving all of the legacy imports and hapi god object references out of the business logic of your plugin and into a legacy _shim_. - -Second, update the consuming code of core services within the plugin to that of the new platform. This can be done in the legacy world, though it is dependent on the relevant services actually existing. - -Once those two things are done, the effort involved in actually updating your plugin to execute in the new plugin system is tiny and non-disruptive. - -Before you do any of that, there are two other things that will make all steps of this process a great deal easier and less risky: switch to TypeScript, and remove your dependencies on angular. - -#### TypeScript - -The new platform does not _require_ plugins to be built with TypeScript, but all subsequent steps of this plan of action are more straightforward and carry a great deal less risk if the code is already converted to TypeScript. - -TypeScript is a superset of JavaScript, so if your goal is the least possible effort, you can move to TypeScript with very few code changes mostly by adding `any` types all over the place. This isn't really any better than regular JavaScript, but simply having your code in `.ts` files means you can at least take advantage of the types that are exported from core and other plugins. This bare minimum approach won't help you much for the architectural shifts, but it could be a great help to you in reliably switching over to new platform services. - -### De-angular - -Angular is not a thing in the new platform. Hopefully your plugin began moving away from angular long ago, but if not, you're in a tight spot. - -If your plugin is registering some sort of global behavior that technically crosses application boundaries, then you have no choice but to get rid of angular. In this case, you're probably best off dealing with this before proceeding with the rest of action plan. - -If your plugin is using angular only in the context of its own application, then removing angular is likely not a definitive requirement for moving to the new platform. In this case, you will need to refactor your plugin to initialize an entirely standalone angular module that serves your application. You will need to create custom wrappers for any of the angular services you previously relied on (including those through `Private()`) inside your own plugin. - -At this point, keeping angular around is not the recommended approach. If you feel you must do it, then talk to the platform team directly and we can help you craft a plan. - -We recommend that _all_ plugins treat moving away from angular as a top-most priority if they haven't done so already. - -### Architectural changes with legacy "shim" - -The bulk of the migration work for most plugins will be changing the way the plugin is architected so dependencies from core and other plugins flow in via the same entry point. This effort is relatively straightforward on the server, but it can be a tremendous undertaking for client-side code in some plugins. - -#### Server-side architectural changes - -Legacy server-side plugins access functionality from core and other plugins at runtime via function arguments, which is similar to how they must be architected to use the new plugin system. - -Let's start with a legacy server-side plugin definition that exposes functionality for other plugins to consume and accesses functionality from both core and a different plugin. - -```ts -export default (kibana) => { - return new kibana.Plugin({ - id: 'demo_plugin', - - init(server) { - server.route({ - path: '/api/demo_plugin/search', - method: 'POST', - async handler(request) { - const { elasticsearch } = server.plugins; - return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); - } - }); - - server.expose('getDemoBar', () => { - return `Demo ${server.plugins.foo.getBar()}`; - }); - } - }); -} -``` - -If we were to express this same set of capabilities in a shape that's more suitable to the new plugin system, it would look something like this: - -```ts -import { CoreSetup } from '../../core/server'; -import { FooPluginSetup } from '../foo/server'; - -interface DemoSetupDependencies { - foo: FooPluginSetup -} - -export type DemoPluginSetup = ReturnType; - -export class Plugin { - public setup(core: CoreSetup, dependencies: DemoSetupDependencies) { - core.http.route({ - path: '/api/demo_plugin/search', - method: 'POST', - async handler(request) { - const { elasticsearch } = core; // note, elasticsearch is moving to core - return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); - } - }); - - return { - getDemoBar() { - return `Demo ${dependencies.foo.getBar()}`; - } - }; - } -} -``` - -Let's break down the key differences in these examples. - -##### Defining the plugin - -The new plugin is defined as a class `Plugin`, whereas the legacy plugin exported a default factory function that instantiated a Kibana-supplied plugin class. Note that there is no id specified on the plugin class itself: - -```ts -// before -export default (kibana) => { - return new kibana.Plugin({ - // ... - }); -} - -// after -export class Plugin { - // ... -} -``` - -##### Starting the plugin up - -The new plugin definition uses `setup` instead of `init`, and rather than getting the hapi server object as its only argument, it gets two arguments: the core services and a dependency on another plugin. - -```ts -// before -init(server) { - // ... -} - -//after -public setup(core: CoreSetup, dependencies: DemoSetupDependencies) { - // ... -} -``` - -##### Accessing core services - -Rather than accessing "core" functions like HTTP routing directly on a hapi server object, the new plugin accesses core functionality through the top level services it exposes in the first argument to `setup`. In the case of HTTP routing, it uses `core.http`. - -```ts -// before -server.route({ - path: '/api/demo_plugin/search', - method: 'POST', - async handler(request) { - const { elasticsearch } = server.plugins; - return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); - } -}); - -// after -core.http.route({ - path: '/api/demo_plugin/search', - method: 'POST', - async handler(request) { - const { elasticsearch } = core; // note, elasticsearch is moving to core - return elasticsearch.getCluster('admin').callWithRequest(request, 'search'); - } -}); -``` - -##### Exposing services for other plugins - -Legacy plugins on the server might expose functionality or services to other plugins by invoking the `expose` function on the hapi `server` object. This can happen at any time throughout the runtime of Kibana which makes it less than reliable. - -New plugins return the contract (if any) that they wish to make available to downstream plugins. This ensures the entirety of a plugin's start contract is available upon completion of its own `setup` function. It also makes it much easier to provide type definitions for plugin contracts. - -```ts -// before -server.expose('getDemoBar', () => { - // ... -}); - -// after -return { - getDemoBar() { - // ... - } -}; -``` - -##### Accessing plugin services - -In server-side code of legacy plugins, you once again use the hapi `server` object to access functionality that was exposed by other plugins. In new plugins, you access the exposed functionality in a similar way but on the second argument to `setup` that is dedicated only to injecting plugin capabilities. - -```ts -// before -server.plugins.foo.getBar() - -// after -dependencies.foo.getBar() -``` - -##### Static files - -One other thing worth noting in this example is how a new plugin will consume static files from core or other plugins, and also how it will expose static files for other plugins. - -This is done through standard modules and relative imports. - -```ts -// import CoreSetup type from core server -import { CoreSetup } from '../../core/server'; - -// import FooPluginSetup type from plugin foo -import { FooPluginSetup } from '../foo/server'; - -// export DemoPluginSetup type for downstream plugins, based on return value of setup() -export type DemoPluginSetup = ReturnType; -``` - -While these particular examples involve only types, the exact same pattern should be followed for those rare situations when a plugin exposes static functionality for plugins to consume. - -##### Rule of thumb for server-side changes - -Outside of the temporary shim, does your plugin code rely directly on hapi.js? If not, you're probably good to go. - -#### Client-side architectural changes - -Client-side legacy plugin code is where things get weird, but the approach is similar - a new plugin definition wraps the business logic of the plugin while legacy functionality is "shimmed" temporarily. Ultimately, there are three high levels goals for client-side architectural changes: - -1. Move all webpack alias imports (`ui/`, `plugin/`, `uiExports/`) into the root shim(s) -2. Adopt global new plugin definitions for all plugins -3. Source of truth for all stateful actions and configuration should originate from new plugin definition - -How you accomplish these things varies wildly depending on the plugin's current implementation and functionality. - -Every plugin will add their global plugin definition via a `hack` uiExport, which will ensure that the plugin definition is always loaded for all applications. This is inline with how the plugin service works in the new platform. - -##### Extending an application - -Let's take a look at a simple plugin that registers functionality to be used in an application. This is done by configuring a uiExport and accessing a registry through a `ui/registry` webpack alias: - -```js -// demo/index.js -{ - uiExports: { - foo: 'plugins/demo/some_file' - } -} - -// demo/public/some_file.js -import chrome from 'ui/chrome'; -import { FooRegistryProvider } from 'ui/registry/foo'; - -FooRegistryProvider.register(() => { - return { - url: chrome.getBasePath() + '/demo_foo' - }; -}); -``` - -To update this plugin, we'll create a plugin definition in a hack uiExport, and we'll move the registration logic there where we'll create some shims into the legacy world. - -```ts -// demo/index.js -{ - uiExports: { - hacks: [ - 'plugins/demo/hacks/shim_plugin' - ] - } -} - -// demo/public/plugin.js -import { CoreSetup } from '../../core/public'; -import { FooPluginSetup } from '../foo/public'; - -interface DemoSetupDependencies { - foo: FooPluginSetup -} - -export class Plugin { - public setup(core: CoreSetup, dependencies: DemoSetupDependencies) { - const { chrome } = core; - - dependencies.foo.registerFoo(() => { - return { - url: chrome.getBasePath() + '/demo_foo' - }; - }); - } -} - -// demo/public/hacks/shim_plugin.js -import chrome from 'ui/chrome'; -import { FooRegistryProvider } from 'ui/registry/foo'; -import { Plugin } from '../plugin'; - -const core = { - chrome: { - getBasePath() { - return chrome.getBasePath(); - } - } -}; -const dependencies = { - foo: { - registerFoo(fn) { - FooRegistryProvider.register(fn); - } - } -}; - -new Plugin().setup(core, dependencies); -``` - -The `shim_plugin.js` file is take on the role of the plugin service in the new platform. It wires up the plugin definition with the dependencies that plugin has on core (i.e. `chrome`) and other plugins (i.e. `foo`). All of the webpack alias imports needed by this plugin have been moved into the shim, and the `plugin.js` code is pristine. - -##### Creating an extension - -Legacy plugins today extend applications by adding functionality through a registry in a uiExport. In the previous example, you saw how to shim this relationship from the extending side into the new plugin definition. Now, let's see how to shim the relavant uiExport registry from the side of the plugin that "owns" it. - -We need to update the registry to expose access to state as an observable. In most cases, this will only affect the implementation details of the owning plugin. This is how state should be shared between plugins in the new platform, but more importantly it is necessary now to move away from the uiExports extension while the order of legacy plugin execution is not determined by a dependency graph. - -In order to support dependent legacy plugins that have not yet been updated, we continue to initiate the uiExport in the app entry file. Once all downstream plugins have been updated to access the registry in a shimmed plugin definition, the `uiExports/` import statement from the app entry file can be removed. - -```ts -// foo/public/plugin.ts -import { ReplaySubject } from 'rxjs'; - -export type FooPluginSetup = ReturnType; - -export type FooFn = () => void; - -export class Plugin { - public constructor() { - this.fooRegistry = []; - this.foos$ = new ReplaySubject(1); - } - - public setup() { - return { - foos$: this.foos$.asObservable(), - registerFoo(fn) { - this.fooRegistry.push(fn); - this.fooSubject.next([ ...this.fooRegistry ]); - } - }; - } - - public stop() { - this.foos$.complete(); - } -} - - -// foo/hacks/shim_plugin.ts -import { Plugin } from '../plugin'; - -const plugin = new Plugin(); -const setup = plugin.setup(); - -require('ui/registry/foo').__temporaryShim__(setup); - - -// ui/public/registry/foo.ts -import { FooPluginSetup, FooFn } from '../../../core_plugins/foo/public/plugin'; - -// legacy plugin order is not guaranteed, so we store a buffer of registry -// calls and then empty them out when the owning plugin shims this module with -// proper state -let temporaryRegistry = []; -let setup; -export function __temporaryShim__(fooSetup: FooPluginSetup) { - if (setup) { - throw new Error('Foo registry already shimmed'); - } - setup = fooSetup; - temporaryRegistry.forEach(fn => setup.registerFoo(fn)); - temporaryRegistry = undefined; -} - -export const FooRegistryProvider = { - register(fn: FooFn) { - if (setup) { - setup.registerFoo(fn); - } else { - temporaryRegistry.push(fn); - } - }, - getAll() { - if (setup) { - return setup.foos$; - } else { - throw new Error('Foo registry not yet shimmed'); - } - } -}; - - -// foo/public/index.js -import 'uiExports/foo'; // to continue support for non-updated plugins - -import { FooRegistryProvider } from 'ui/registry/foo'; -FooRegistryProvider.getAll().subscribe(foos => { - // do something with array foos -}); -``` - -##### Plugin with generic application - -It is difficult to provide concrete guidance for migrating an application because most applications have unique architectures and bootstrapping logic. - -In the new plugin system, a plugin registers an application via the application service by providing an asyncronous `mount` function that core invokes when a user attempts to load the app. In this mounting function, a plugin would use an async import statement to load the application code so it wasn't loaded by the browser until it was first navigated to. - -The basic interface would be something similar to: - -```ts -async function mount({ domElement }) { - const { bootstrapApp } = await import('/application'); - - const unmount = bootstrapApp({ domElement }); - - // returns a function that cleans up after app when navigating away - return unmount; -} - -// application.ts -export function bootstrapApp({ domElement }) { - // all of the application-specific setup logic - // application renders into the given dom element -} -``` - -This pattern provides flexibility for applications to have bootstrap logic and technologies that are not prescribed by or coupled to core itself. - -Applications in legacy plugins are instead resolved on the server-side and then get served to the client via application-specific entry files. Migrating applications involves adopting the above pattern for defining mounting logic in the global plugin definition hack, and then updating the legacy app entry file to behave like the new platform core will by invoking the mounting logic. - -As before, shims will need to be created for legacy integrations with core and other plugins, and all webpack alias-based imports will need to move to those shims. - -**React application:** - -Let's look at an example for a react application. - -```ts -// demo/public/plugin.js -export class Plugin { - setup(core) { - const { I18nContext } = core.i18n; - core.applications.register('demo', async function mount(domElement) { - const { bootstrapApp } = await import('../application'); - return bootstrapApp({ domElement, I18nContext }); - }); - } -} - - -// demo/public/application.js -import React from 'react'; -import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; -import ReactDOM from 'react-dom'; - -import { configureStore } from './store'; -import { Main } from './components/Main'; - -import './style/demo_custom_styles.css'; - -export function bootstrapApp({ domElement, I18nContext }) { - const store = configureStore(); - - ReactDOM.render( - - - -
- - - , - domElement - ); - - return function destroyApp() { - ReactDOM.unmountComponentAtNode(domElement); - } -} - - -// demo/public/hacks/shim_plugin.js -import { coreSetup } from 'ui/core'; -import { I18nContext } from 'ui/i18n'; -import { Plugin } from '../plugin'; - -const core = { - ...coreSetup, - i18n: { - I18nContext - } -}; - -new Plugin().setup(core); - - -// demo/public/index.js -import { coreInternals } from 'ui/core'; -import 'ui/autoload/styles'; - -chrome.setRootController(function ($element) { - coreInternals.applications.mountApp('demo', $element[0]); -}); -``` - -The plugin and application bundles do not use webpack aliases for imports. Stateful services are passed around as function arguments. The plugin definition shim wires up the necessary bits of the core setup contract, and the legacy app entry file shims the app-specific behaviors that ultimately will move into core (e.g. mounting the application) or will go away entirely (ui/autoload/styles). - -**Angular application:** - -WIP - -Angular applications must be handled a little differently since angular is still temporarily a part of core, and you cannot easily embed isolated angular applications within one another at runtime. Angular will be moved out of core and into individual plugins in 7.x, but in the meantime the angular application logic should continue to be bootstrapped through the legacy app entry file. - -The best possible outcome is for applications to remove angular entirely in favor of React, but if the intention is to preserve the angular app post-migration, then the focus today should be on removing dependencies on external services (provided by core or other plugins) that are injected through the angular dependency injection mechanism. - -In the future, services provided by core and other plugins will not be available automatically via the angular dependency injection system. To prepare for that inevitability, angular applications should be updated to define those services themselves. For now, this can be done through shims. - -Let's consider the following example that relies on the core `chrome` service accessed through the angular dependency injection mechanism. - -```js -// demo/public/index.js -import routes from 'ui/routes'; - -import './directives'; -import './services'; - -routes.enable(); -routes.when('/demo', { - controller(chrome) { - this.basePath = chrome.getBasePath(); - } -}); -``` - -The demo application does not "own" `chrome`, so it won't exist automatically in the future. To continue using it, the demo application will need to configure it - - - - -# Random temporary idea thrashing below - - -```js -// visualize/index.js -// example of app entry file for upgraded angular plugin -import chrome from 'ui/chrome'; -import routes from 'ui/routes'; -import { uiModules } from 'ui/modules'; - -import 'uiExports/visTypes'; - -import 'ui/autoload/all'; -import './visualize'; -import 'ui/vislib'; -import { showAppRedirectNotification } from 'ui/notify'; - -import { application } from 'ui/core'; - -chrome.setRootController(class { - constructor($element) { - core.applications.mountApp('demo', $element[0]); - } -}); - -import template from './templates/index.html'; -chrome.setRootTemplate(template); -initTimepicker().then(() => { - -}) - - -routes.enable(); - -routes - .otherwise({ - redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}` - }); - -uiModules.get('kibana').run(showAppRedirectNotification); - -bootstrapAngularChrome(); -``` - - -##### Rule of thumb for client-side changes - -Outside of the temporary shim, does your plugin code rely directly on code imported from webpack aliases (e.g. `import from 'plugins/...'` or `import from 'ui/...'`)? If not, you're probably good to go. From b97ad579933cae8cdfb753729b1196817a6e62c0 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 15 Mar 2019 15:25:37 -0400 Subject: [PATCH 34/46] faq --- src/core/MIGRATION.md | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index e412b51dd1e73..59f9fea2c39ac 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -621,3 +621,56 @@ With all of your services converted, you are now ready to complete your migratio Details to come... + +## Frequently asked questions + +### Is migrating a plugin an all-or-nothing thing? + +It doesn't have to be. Within the Kibana repo, you can have a new platform plugin with the same name as a legacy plugin. + +Technically speaking, you could move all of your server-side code to the new platform and leave the legacy browser-side code where it is. You can even move only a portion of code on your server at a time, like on a route by route basis for example. + +For any new plugin APIs being defined as part of this process, it is recommended to create those APIs in new platform plugins, and then core will pass them down into the legacy world to be used there. This leaves one less thing you need to migrate. + +### Do plugins need to be converted to TypeScript? + +No. That said, the migration process will require a lot of refactoring, and TypeScript will make this dramatically easier and less risky. Independent of the new platform effort, our goals are to convert the entire Kibana repo to TypeScript over time, so now is a great time to do it. + +At the very least, any plugin exposing an extension point should do so with first-class type support so downstream plugins that _are_ using TypeScript can depend on those types. + +### How is static code shared between plugins? + +Plugins are strongly discouraged from sharing static code for other plugins to import. There will be times when it is necessary, so it will remain possible, but it has serious drawbacks that won't necessarily be clear at development time. + +1. When a plugin is uninstalled, its code is removed from the filesystem, so all imports referencing it will break. This will result in Kibana failing to start or load, and there is no way to recover beyond installing the missing plugin or disabling the plugin with the broken import. +2. When a plugin is disabled, its static exports will still be importable by any other plugin. This can result in undesirable effects where it _appears_ like a plugin is enabled when it is not. In the worst case, it can result in an unexpected user experience where features that should have been disabled are not. +3. Code that is statically imported will be _copied_ into the plugin that imported it. This will bloat your plugin's client-side bundles and its footprint on the server's file system. Often today client-side imports expose a global singleton, and due to this copying behavior that will no longer work. + +If you must share code statically, regardless of whether static code is on the server or in the browser, it can be imported via relative paths. + +For some background, this has long been problemmatic in Kibana for two reasons: + +* Plugin directories were configurable, so there was no reliably relative path for imports across plugins and from core. This has since been addressed and all plugins in the Kibana repo have reliable locations relative to the Kibana root. +* The `x-pack` directory moved into `node_modules` at build time, so a relative import from `x-pack` to `src` that worked during development would break once a Kibana distribution was built. This is still a problem today, but the fix is in flight via issue [#32722](https://github.com/elastic/kibana/pull/32722). + +Any code not exported via the index of either the `server` or `public` directories should never be imported outside that plugin as it should be considered unstable and subject to change at any time. + +### How is "common" code shared on both the client and server? + +There is no formal notion of "common" code that can safely be imported from either client-side or server-side code. However, if a plugin author wishes to maintain a set of code in their plugin in a single place and then expose it to both server-side and client-side code, they can do so by exporting in the index files for both the `server` and `public` directories. + +The benefit of this approach is that the details of where code lives and whether it is accessible in multiple runtimes is an implementation detail of the plugin itself. A plugin consumer that is writing client-side code only ever needs to concern themselves with the client-side contracts being exposed, and the same can be said for server-side contracts on the server. + +A plugin author that decides some set of code should diverge from having a single "common" definition can now safely change the implementation details without impacting downstream consumers. + +### When does code go into a plugin, core, or packages? + +This is an impossible question to answer definitively for all circumstances. For each time this question is raised, we must carefully consider to what extent we think that code is relevant to almost everyone developing in Kbana, what license the code is shipping under, which teams are most appropriate to "own" that code, is the code stateless etc. + +As a general rule of thumb, most code in Kibana should exist in plugins. Plugins are the most obvious way that we break Kibana down into sets of specialized domains with controls around interdependency communication and management. It's always possible to move code from a plugin into core if we ever decide to do so, but it's much more disruptive to move code from core to a plugin. + +There is essentially no code that _can't_ exist in a plugin. When in doubt, put the code in a plugin. + +After plugins, core is where most of the rest of the code in Kibana will exist. Functionality that's critical to the reliable execution of the Kibana process belongs in core. Services that will widely be used by nearly every non-trivial plugin in any Kibana install belong in core. Functionality that is too specialized to specific use cases should not be in core, so while something like generic saved objects is a core concern, index patterns are not. + +The packages directory should have the least amount of code in Kibana. Just because some piece of code is not stateful doesn't mean it should go into packages. The packages directory exists to aid us in our quest to centralize as many of our owned dependencies in this single monorepo, so it's the logical place to put things like Elastic forks of node modules or vendored dependencies. From fa654474a0dc4d75a502d9f7a65563c4c43b810d Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 16 Mar 2019 13:06:37 -0400 Subject: [PATCH 35/46] Revert "pseudo code for loading np browser shims" This reverts commit e720b27d92f421a71c881d4416673106aecbc7af. --- src/core/public/core_system.ts | 4 ++-- src/core/public/legacy/legacy_service.ts | 24 +----------------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 0e96e659103d4..ea934815fd448 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -100,7 +100,7 @@ export class CoreSystem { }); } - public async start() { + public start() { try { // ensure the rootDomElement is empty this.rootDomElement.textContent = ''; @@ -125,7 +125,7 @@ export class CoreSystem { notifications, }); - await this.legacyPlatform.start({ + this.legacyPlatform.start({ i18n, injectedMetadata, fatalErrors, diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 1dae03f70509d..6ab8b912bfcef 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -54,7 +54,7 @@ export interface LegacyPlatformParams { export class LegacyPlatformService { constructor(private readonly params: LegacyPlatformParams) {} - public async start(deps: Deps) { + public start(deps: Deps) { const { i18n, injectedMetadata, @@ -86,28 +86,6 @@ export class LegacyPlatformService { // the bootstrap module can modify the environment a bit first const bootstrapModule = this.loadBootstrapModule(); - // emulates new platform-like loading cycle - // can replace hacks and chrome uiExports - - // shim plugin instantiation - // @ts-ignore - const plugins = injectedMetadata.getSortedPluginNames().map((name: string) => { - const { shim } = require(`plugins/${name}/np_plugin`); - return { name, plugin: shim() }; - }); - - // shim setup task - for (const { name, plugin } of plugins) { - const { shim } = require(`plugins/${name}/np_plugin_setup`); - await shim(plugin); - } - - // shim start task - for (const { name, plugin } of plugins) { - const { shim } = require(`plugins/${name}/np_plugin_start`); - await shim(plugin); - } - // require the files that will tie into the legacy platform this.params.requireLegacyFiles(); From 7783af04cae30db58a015258d50aad44fcdb1eaf Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 16 Mar 2019 13:09:16 -0400 Subject: [PATCH 36/46] fix typo in browser details --- src/core/MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 59f9fea2c39ac..fe90eff1e453b 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -595,7 +595,7 @@ All teams that own a plugin are strongly encouraged to remove angular entirely, One way to address this problem is to go through the code that is currently exposed to plugins and refactor away all of the touch points into angular.js. This might be the easiest option in some cases, but it might be hard in others. -Another way to address this problem is to create an entirely new set of plugin APIs that are not dependendent on angular.js, and then update the implementation within the plugin to "merge" the angular and non-angular capabilities together. This is a good approach if preserving the existing angular API until we remove the old plugin system entirely is of critical importance. Generally speaking though, the removal of angular and introduction of a new set of public plugin APIs is a good reason to make a breaking change to the existing plugin capabilities. Make sure the PRs are tagged appropriate so we add these changes to our plugin changes blog post for each release. +Another way to address this problem is to create an entirely new set of plugin APIs that are not dependendent on angular.js, and then update the implementation within the plugin to "merge" the angular and non-angular capabilities together. This is a good approach if preserving the existing angular API until we remove the old plugin system entirely is of critical importance. Generally speaking though, the removal of angular and introduction of a new set of public plugin APIs is a good reason to make a breaking change to the existing plugin capabilities. Make sure the PRs are tagged appropriately so we add these changes to our plugin changes blog post for each release. ### Move all webpack alias imports into apiExport entry files From c511f75c9c05309e55543139970f510ec2950d67 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 16 Mar 2019 13:15:12 -0400 Subject: [PATCH 37/46] final migration notes --- src/core/MIGRATION.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index fe90eff1e453b..2df62fadf5c0a 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -561,7 +561,9 @@ At this point, your legacy server-side plugin's logic is no longer coupled to le With both shims converted, you are now ready to complete your migration to the new platform. -Details to come... +Many plugins will copy and paste all of their plugin code into a new plugin directory and then delete their legacy shims. + +With the previous steps resolved, this final step should be easy, but the exact process may vary plugin by plugin, so when you're at this point talk to the platform team to figure out the exact changes you need. ## Browser-side plan of action @@ -619,8 +621,9 @@ Once all of the existing webpack alias-based imports in your plugin switch to `u With all of your services converted, you are now ready to complete your migration to the new platform. -Details to come... +Many plugins at this point will create a new plugin definition class and copy and paste the code from their various uiExport entry files directly into the new plugin class. The legacy uiExport entry files can then simply be deleted. +With the previous steps resolved, this final step should be easy, but the exact process may vary plugin by plugin, so when you're at this point talk to the platform team to figure out the exact changes you need. ## Frequently asked questions From 961b9ef8aa68fe137ebb5c71868802317dbafdaf Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 16 Mar 2019 13:16:54 -0400 Subject: [PATCH 38/46] talk to platform about apis and bundling --- src/core/MIGRATION.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 2df62fadf5c0a..3df64dde38add 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -599,6 +599,8 @@ One way to address this problem is to go through the code that is currently expo Another way to address this problem is to create an entirely new set of plugin APIs that are not dependendent on angular.js, and then update the implementation within the plugin to "merge" the angular and non-angular capabilities together. This is a good approach if preserving the existing angular API until we remove the old plugin system entirely is of critical importance. Generally speaking though, the removal of angular and introduction of a new set of public plugin APIs is a good reason to make a breaking change to the existing plugin capabilities. Make sure the PRs are tagged appropriately so we add these changes to our plugin changes blog post for each release. +Please talk with the platform team when formalizing _any_ client-side extension points that you intend to move to the new platform as there are some bundling considerations to consider. + ### Move all webpack alias imports into apiExport entry files Existing plugins import three things using webpack aliases today: services from ui/public (`ui/`), services from other plugins (`plugins/`), and uiExports themselves (`uiExports/`). These webpack aliases will not exist once we remove the legacy plugin system, so part of our migration effort is addressing all of the places where they are used today. From 5a646496fa5d057bff9c34eaa9ed463b234a80e6 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 16 Mar 2019 13:24:59 -0400 Subject: [PATCH 39/46] links in outline --- src/core/MIGRATION.md | 46 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 3df64dde38add..4cc76b2666579 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1,28 +1,28 @@ # Migrating legacy plugins to the new platform -* Overview - * Architectural - * Services - * Integrating with other plugins - * Challenges to overcome with legacy plugins - * Plan of action -* Server-side plan of action - * De-couple from hapi.js server and request objects - * Introduce new plugin definition shim - * Switch to new platform services - * Migrate to the new plugin system -* Browser-side plan of action - * Move UI modules into plugins - * Provide plugin extension points decoupled from angular.js - * Move all webpack alias imports into apiExport entry files - * Switch to new platform services - * Migrate to the new plugin system -* Frequently asked questions - * Is migrating a plugin an all-or-nothing thing? - * Do plugins need to be converted to TypeScript? - * How is static code shared between plugins? - * How is "common" code shared on both the client and server? - * When does code go into a plugin, core, or packages? +* [Overview](#overview) + * [Architectural](#architectural) + * [Services](#services) + * [Integrating with other plugins](#integrating-with-other-plugins) + * [Challenges to overcome with legacy plugins](#challenges-to-overcome-with-legacy-plugins) + * [Plan of action](#plan-of-action) +* [Server-side plan of action](#server-side-plan-of-action) + * [De-couple from hapi.js server and request objects](#de-couple-from-hapijs-server-and-request-objects) + * [Introduce new plugin definition shim](#introduce-new-plugin-definition-shim) + * [Switch to new platform services](#switch-to-new-platform-services) + * [Migrate to the new plugin system](#migrate-to-the-new-plugin-system) +* [Browser-side plan of action](#browser-side-plan-of-action) + * [Move UI modules into plugins](#move-ui-modules-into-plugins) + * [Provide plugin extension points decoupled from angular.js](#provide-plugin-extension-points-decoupled-from-angularjs) + * [Move all webpack alias imports into apiExport entry files](#move-all-webpack-alias-imports-into-apiexport-entry-files) + * [Switch to new platform services](#switch-to-new-platform-services-1) + * [Migrate to the new plugin system](#migrate-to-the-new-plugin-system-1) +* [Frequently asked questions](#frequently-asked-questions) + * [Is migrating a plugin an all-or-nothing thing?](#is-migrating-a-plugin-an-all-or-nothing-thing) + * [Do plugins need to be converted to TypeScript?](#do-plugins-need-to-be-converted-to-typescript) + * [How is static code shared between plugins?](#how-is-static-code-shared-between-plugins) + * [How is "common" code shared on both the client and server?](#how-is-common-code-shared-on-both-the-client-and-server) + * [When does code go into a plugin, core, or packages?](#when-does-code-go-into-a-plugin-core-or-packages) Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. From 689a662920f4ca5571b1ce798db11ea5817dbd06 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Sat, 16 Mar 2019 13:26:26 -0400 Subject: [PATCH 40/46] typo in outline --- src/core/MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 4cc76b2666579..af909048b9f25 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1,7 +1,7 @@ # Migrating legacy plugins to the new platform * [Overview](#overview) - * [Architectural](#architectural) + * [Architecture](#architecture) * [Services](#services) * [Integrating with other plugins](#integrating-with-other-plugins) * [Challenges to overcome with legacy plugins](#challenges-to-overcome-with-legacy-plugins) From 4e714f9c16079fe5b3fc424bbff371054a690373 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 18 Mar 2019 12:38:53 +0100 Subject: [PATCH 41/46] Update MIGRATION.md --- src/core/MIGRATION.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index af909048b9f25..03e834150fbef 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -257,7 +257,7 @@ The approach and level of effort varies significantly between server and browser First, decouple your plugin's business logic from the dependencies that are not exposed through the new platform, hapi.js and angular.js. Then introduce plugin definitions that more accurately reflect how plugins are defined in the new platform. Finally, replace the functionality you consume from core and other plugins with their new platform equivalents. -Once those things are finished for any given plugin, it can offically be switched to the new plugin system. +Once those things are finished for any given plugin, it can officially be switched to the new plugin system. ## Server-side plan of action @@ -597,7 +597,7 @@ All teams that own a plugin are strongly encouraged to remove angular entirely, One way to address this problem is to go through the code that is currently exposed to plugins and refactor away all of the touch points into angular.js. This might be the easiest option in some cases, but it might be hard in others. -Another way to address this problem is to create an entirely new set of plugin APIs that are not dependendent on angular.js, and then update the implementation within the plugin to "merge" the angular and non-angular capabilities together. This is a good approach if preserving the existing angular API until we remove the old plugin system entirely is of critical importance. Generally speaking though, the removal of angular and introduction of a new set of public plugin APIs is a good reason to make a breaking change to the existing plugin capabilities. Make sure the PRs are tagged appropriately so we add these changes to our plugin changes blog post for each release. +Another way to address this problem is to create an entirely new set of plugin APIs that are not dependent on angular.js, and then update the implementation within the plugin to "merge" the angular and non-angular capabilities together. This is a good approach if preserving the existing angular API until we remove the old plugin system entirely is of critical importance. Generally speaking though, the removal of angular and introduction of a new set of public plugin APIs is a good reason to make a breaking change to the existing plugin capabilities. Make sure the PRs are tagged appropriately so we add these changes to our plugin changes blog post for each release. Please talk with the platform team when formalizing _any_ client-side extension points that you intend to move to the new platform as there are some bundling considerations to consider. @@ -653,7 +653,7 @@ Plugins are strongly discouraged from sharing static code for other plugins to i If you must share code statically, regardless of whether static code is on the server or in the browser, it can be imported via relative paths. -For some background, this has long been problemmatic in Kibana for two reasons: +For some background, this has long been problematic in Kibana for two reasons: * Plugin directories were configurable, so there was no reliably relative path for imports across plugins and from core. This has since been addressed and all plugins in the Kibana repo have reliable locations relative to the Kibana root. * The `x-pack` directory moved into `node_modules` at build time, so a relative import from `x-pack` to `src` that worked during development would break once a Kibana distribution was built. This is still a problem today, but the fix is in flight via issue [#32722](https://github.com/elastic/kibana/pull/32722). @@ -670,7 +670,7 @@ A plugin author that decides some set of code should diverge from having a singl ### When does code go into a plugin, core, or packages? -This is an impossible question to answer definitively for all circumstances. For each time this question is raised, we must carefully consider to what extent we think that code is relevant to almost everyone developing in Kbana, what license the code is shipping under, which teams are most appropriate to "own" that code, is the code stateless etc. +This is an impossible question to answer definitively for all circumstances. For each time this question is raised, we must carefully consider to what extent we think that code is relevant to almost everyone developing in Kibana, what license the code is shipping under, which teams are most appropriate to "own" that code, is the code stateless etc. As a general rule of thumb, most code in Kibana should exist in plugins. Plugins are the most obvious way that we break Kibana down into sets of specialized domains with controls around interdependency communication and management. It's always possible to move code from a plugin into core if we ever decide to do so, but it's much more disruptive to move code from core to a plugin. From a48e389da837cb286d4d3e5e23e55d85e27a9303 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Mon, 18 Mar 2019 17:10:22 +0200 Subject: [PATCH 42/46] Update MIGRATION.md - Add missing return statement highlighted during meeting. - `apiExport` -> `uiExport` - Small spelling check --- src/core/MIGRATION.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 03e834150fbef..c8dfeb89e42bb 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -14,7 +14,7 @@ * [Browser-side plan of action](#browser-side-plan-of-action) * [Move UI modules into plugins](#move-ui-modules-into-plugins) * [Provide plugin extension points decoupled from angular.js](#provide-plugin-extension-points-decoupled-from-angularjs) - * [Move all webpack alias imports into apiExport entry files](#move-all-webpack-alias-imports-into-apiexport-entry-files) + * [Move all webpack alias imports into uiExport entry files](#move-all-webpack-alias-imports-into-uiexport-entry-files) * [Switch to new platform services](#switch-to-new-platform-services-1) * [Migrate to the new plugin system](#migrate-to-the-new-plugin-system-1) * [Frequently asked questions](#frequently-asked-questions) @@ -170,8 +170,10 @@ export class Plugin { } public stop() { - getBar() { - return 'bar'; + return { + getBar() { + return 'bar'; + } } } } @@ -273,7 +275,7 @@ The server object is introduced to your plugin in its legacy `init` function, so The `request` object is introduced to your plugin in every route handler, so at the root of every route handler, you will create a new interface by "picking" the request information (e.g. body, headers) and core and plugin capabilities from the `request` object that you actually use and pass that in all the places you previously were passing `request`. -Any calls to mutate either the server or request objects (e.g. server.decorate()) will be moved toward the root of the legacy `init` function if they aren't already there. +Any calls to mutate either the server or request objects (e.g. `server.decorate()`) will be moved toward the root of the legacy `init` function if they aren't already there. Let's take a look at an example legacy plugin definition that uses both `server` and `request`. @@ -355,7 +357,7 @@ export default (kibana) => { } ``` -This change might seem trivial, but its important for two reasons. +This change might seem trivial, but it's important for two reasons. First, the business logic built into `search` is now coupled to an object you created manually and have complete control over rather than hapi itself. This will allow us in a future step to replace the dependency on hapi without necessarily having to modify the business logic of the plugin. @@ -363,7 +365,7 @@ Second, it forced you to clearly define the dependencies you have on capabilitie ### Introduce new plugin definition shim -While most plugin logic is now decoupled from hapi, the plugin definition itself still uses hapi to expose functionality for other plugins to consume and accesses functionality from both core and a different plugin. +While most plugin logic is now decoupled from hapi, the plugin definition itself still uses hapi to expose functionality for other plugins to consume and access functionality from both core and a different plugin. ```ts // index.ts @@ -526,7 +528,7 @@ init(server) { } ``` -At this point, your legacy server-side plugin's logic is no longer coupled to the legacy core. +At this point, your legacy server-side plugin logic is no longer coupled to the legacy core. A similar approach can be taken for your plugins shim. First, update your plugin shim in `init` to extend `server.newPlatform.setup.plugins`. @@ -555,7 +557,7 @@ init(server) { } ``` -At this point, your legacy server-side plugin's logic is no longer coupled to legacy plugins. +At this point, your legacy server-side plugin logic is no longer coupled to legacy plugins. ### Migrate to the new plugin system @@ -601,7 +603,7 @@ Another way to address this problem is to create an entirely new set of plugin A Please talk with the platform team when formalizing _any_ client-side extension points that you intend to move to the new platform as there are some bundling considerations to consider. -### Move all webpack alias imports into apiExport entry files +### Move all webpack alias imports into uiExport entry files Existing plugins import three things using webpack aliases today: services from ui/public (`ui/`), services from other plugins (`plugins/`), and uiExports themselves (`uiExports/`). These webpack aliases will not exist once we remove the legacy plugin system, so part of our migration effort is addressing all of the places where they are used today. From 602091982d2d1c48ad464304f1e05b65d98f14a5 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Mon, 18 Mar 2019 19:44:58 -0400 Subject: [PATCH 43/46] different wording on packages --- src/core/MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index c8dfeb89e42bb..93c9ee46d57ff 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -680,4 +680,4 @@ There is essentially no code that _can't_ exist in a plugin. When in doubt, put After plugins, core is where most of the rest of the code in Kibana will exist. Functionality that's critical to the reliable execution of the Kibana process belongs in core. Services that will widely be used by nearly every non-trivial plugin in any Kibana install belong in core. Functionality that is too specialized to specific use cases should not be in core, so while something like generic saved objects is a core concern, index patterns are not. -The packages directory should have the least amount of code in Kibana. Just because some piece of code is not stateful doesn't mean it should go into packages. The packages directory exists to aid us in our quest to centralize as many of our owned dependencies in this single monorepo, so it's the logical place to put things like Elastic forks of node modules or vendored dependencies. +The packages directory should have the least amount of code in Kibana. Just because some piece of code is not stateful doesn't mean it should go into packages. The packages directory exists to aid us in our quest to centralize as many of our owned dependencies in this single monorepo, so it's the logical place to put things like Kibana specific forks of node modules or vendor dependencies. From 5d343de2fcc4452470bc7bd3e6d9b9e51e459b13 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 5 Apr 2019 11:54:57 -0400 Subject: [PATCH 44/46] lifecycle function details --- src/core/MIGRATION.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 93c9ee46d57ff..35f6d12cbf3cd 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -130,9 +130,13 @@ The platform does not impose any technical restrictions on how the internals of ### Services -The various independent domains that make up `core` are represented by a series of services, and many of those services expose public interfaces that are provided to _all_ plugins via the first argument of their `setup` and `stop` functions. The interface varies from service to service, but it is always accessed through this argument. +The various independent domains that make up `core` are represented by a series of services, and many of those services expose public interfaces that are provided to _all_ plugins. Services expose different features at different parts of their _lifecycle_. We describe the lifecycle of core services and plugins with specifically-named functions on the service definition. -For example, the core `UiSettings` service exposes a function `get` to all plugin `setup` functions. To use this function to retrieve a specific UI setting, a plugin just accesses it off of the first argument: +In the new platform, there are two lifecycle functions today: `setup` and `stop`. The `setup` functions are invoked sequentially while Kibana is starting up on the server or when it is being loaded in the browser. The `stop` functions are invoked sequentially while Kibana is gracefully shutting down on the server or when the browser tab or window is being closed. + +There is no equivalent behavior to `stop` in legacy plugins, so this guide primarily focuses on migrating functionality into `setup`. + +The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. For example, the core `UiSettings` service exposes a function `get` to all plugin `setup` functions. To use this function to retrieve a specific UI setting, a plugin just accesses it off of the first argument: ```ts import { CoreSetup } from '../../../core/public'; @@ -148,9 +152,11 @@ Different service interfaces can and will be passed to `setup` and `stop` becaus For example, the `stop` function in the browser gets invoked as part of the `window.onbeforeunload` event, which means you can't necessarily execute asynchronous code here in a reliable way. For that reason, `core` likely wouldn't provide any asynchronous functions to plugin `stop` functions in the browser. +Core services that expose functionality to plugins always have their `setup` function ran before any plugins. + ### Integrating with other plugins -Plugins can expose public interfaces for other plugins to consume. Like `core`, those interfaces are bound to `setup` and/or `stop`. +Plugins can expose public interfaces for other plugins to consume. Like `core`, those interfaces are bound to the lifecycle functions `setup` and/or `stop`. Anything returned from `setup` or `stop` will act as the interface, and while not a technical requirement, all Elastic plugins should expose types for that interface as well. From 5f79b103266ea416cc92c97ac27107b0fbc279bd Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 5 Apr 2019 12:00:26 -0400 Subject: [PATCH 45/46] PluginStop -> CoreStop --- src/core/MIGRATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 35f6d12cbf3cd..b93a984633e5d 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -110,7 +110,7 @@ export function plugin(initializerContext: PluginInitializerContext) { **[5] `server/plugin.ts`** is the server-side plugin definition. The _shape_ of this plugin is the same as it's client-side counter-part: ```ts -import { PluginInitializerContext, CoreSetup, PluginStop } from '../../../core/server'; +import { PluginInitializerContext, CoreSetup, CoreStop } from '../../../core/server'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { @@ -120,7 +120,7 @@ export class Plugin { // called when plugin is setting up during Kibana's startup sequence } - public stop(core: PluginStop) { + public stop(core: CoreStop) { // called when plugin is torn down during Kibana's shutdown sequence } } From 471ba43c86ffba87e6fc3053de7a2c11e1631bf6 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 5 Apr 2019 12:06:24 -0400 Subject: [PATCH 46/46] dependencies -> plugins --- src/core/MIGRATION.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index b93a984633e5d..e2c30ff95585d 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -94,7 +94,7 @@ export class Plugin { // called when plugin is torn down, aka window.onbeforeunload } } -``` +```x **[4] `server/index.ts`** is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: @@ -208,23 +208,23 @@ With that specified in the plugin manifest, the appropriate interfaces are then import { CoreSetup, PluginStop } from '../../../core/server'; import { FoobarPluginSetup, FoobarPluginStop } from '../../foobar/server'; -interface DemoSetupDependencies { +interface DemoSetupPlugins { foobar: FoobarPluginSetup } -interface DemoStopDependencies { +interface DemoStopPlugins { foobar: FoobarPluginStop } export class Plugin { - public setup(core: CoreSetup, dependencies: DemoSetupDependencies) { - const { foobar } = dependencies; + public setup(core: CoreSetup, plugins: DemoSetupPlugins) { + const { foobar } = plugins; foobar.getFoo(); // 'foo' foobar.getBar(); // throws because getBar does not exist } - public stop(core: PluginStop, dependencies: DemoStopDependencies) { - const { foobar } = dependencies; + public stop(core: PluginStop, plugins: DemoStopPlugins) { + const { foobar } = plugins; foobar.getFoo(); // throws because getFoo does not exist foobar.getBar(); // 'bar' } @@ -422,14 +422,14 @@ interface FooSetup { getBar(): string } -interface DependenciesSetup { +interface PluginsSetup { foo: FooSetup } export type DemoPluginSetup = ReturnType; export class Plugin { - public setup(core: CoreSetup, dependencies: DependenciesSetup) { + public setup(core: CoreSetup, plugins: PluginsSetup) { const serverFacade: ServerFacade = { plugins: { elasticsearch: core.elasticsearch @@ -451,7 +451,7 @@ export class Plugin { // Exposing functionality for other plugins return { getDemoBar() { - return `Demo ${dependencies.foo.getBar()}`; // Accessing functionality from another plugin + return `Demo ${plugins.foo.getBar()}`; // Accessing functionality from another plugin } }; } @@ -478,11 +478,11 @@ export default (kibana) => { } }; // plugins shim - const dependenciesSetup = { + const pluginsSetup = { foo: server.plugins.foo }; - const demoSetup = new Plugin().setup(coreSetup, dependenciesSetup); + const demoSetup = new Plugin().setup(coreSetup, pluginsSetup); // continue to expose functionality to legacy plugins server.expose('getDemoBar', demoSetup.getDemoBar); @@ -541,7 +541,7 @@ A similar approach can be taken for your plugins shim. First, update your plugin ```ts init(server) { // plugins shim - const dependenciesSetup = { + const pluginsSetup = { ...server.newPlatform.setup.plugins, foo: server.plugins.foo }; @@ -552,12 +552,12 @@ As the plugins you depend on are migrated to the new platform, their contract wi It is much easier to reliably make breaking changes to plugin APIs in the new platform than it is in the legacy world, so if you're planning a big change, consider doing it after your dependent plugins have migrated rather than as part of your own migration. -Eventually, all overrides will be removed and your `dependenciesSetup` shim is entirely powered by `server.newPlatform.setup.plugins`. +Eventually, all overrides will be removed and your `pluginsSetup` shim is entirely powered by `server.newPlatform.setup.plugins`. ```ts init(server) { // plugins shim - const dependenciesSetup = { + const pluginsSetup = { ...server.newPlatform.setup.plugins }; }