The client-side application service handles registering new applications. It should expose a client for mounting and unmounting applications, which will ultimately be used as a sort of base router in the UI. It might also want to manage application state, like whether the application is disabled or not, or at least provide a mechanism to plugins to indicate state.
Routing RFC: https://github.com/elastic/kibana/pull/36477
kbn-chrome directive for rendering legacy apps.The exploration below resulted in us deciding _against_ using iframes.
I've done some exploration on syncing the window.history in an iframe with the parent and have put together a small demo here. Main conclusions:
window.history changes works and could be extended easily to also support changes to window.location.hash by watching the hashchanged event on the iframe's window.window.location = '/url' without polling, 馃憥. The browser does not let us redefine this property descriptor in order to capture the setter of this property.window. Because we're on the same domain, I don't believe we have to use the postMessage API.One of the hairy parts of rendering inside iframes would be getting the actual application to load inside the iframe and still be able to access core services or plugin-specific code.
One way I think of solving this would be to require that applications provide a separate entry file that the ApplicationService would load for them. This entry bundle could expose a mount function that ApplicationService would call with specified arguments. The main drawback of this approach is that the mount function is not a closure inside the plugin's setup callback like we've been envisioning.
// plugin.js
class Plugin {
setup(core) {
core.applicationService.registerApplication({
name: 'Dashboard',
route: '/dashboard',
icon: <EuiIcon type="dashboardApp" />,
entryPoint: './my_entry_point.js',
// Application service would call window.mount with this array
// as it's args.
mountArgs: [{
kfetch: core.kfetch,
myService: {
getObj() { ... },
}
}]
});
}
}
// my_entry_point.js
window.__kbnMount__ = (targetDomElement, { kfetch, myService }) => {
ReactDOM.mount(<MyApp />, targetDomElement);
return () => {
ReactDOM.unmountComponentAtNode(targetDomElement);
};
};
This would introduce an implicit contract where the app service executes window.__kbnMount__ that was created by the my_entry_point.js file when loaded on an HTML page. Type-safety for this bundle would not be guaranteed, but could be enforced by exporting a type to represent the parameters of your mount function.
This method would provide us more "sandboxing" which could be particularly useful for 3rd-party plugins. They may not know about our custom angular-route hacks to make Angular.js v1 work on new Kibana. Any other frameworks they use that do any global monkey-patching would be safe to use.
The downside is that this solution is brittle, could break in some browsers (so far, I've only tested in Firefox), and may introduce security concerns that need to be handled.
@eliperelman and I synced today and here is our tentative plan:
_Moved to description above_
After some investigations on (3) above, I've decided that completely moving the "lastSubUrl" feature into plugins is not feasible until plugins are on the new platform.
The reason for this boils down to the fact that a legacy Angular app does not know when it is being "mounted" by Chrome vs. the user trying to get back to the root of the app. Without this, an individual plugin cannot make the decision of whether or not they should redirect to the last known location or render the root of the app.
Instead, I will modify the existing lastSubUrl logic to manually modify navLinks in the new platform ChromeService. However, I will not expose this functionality to new platform plugins. New platform plugins will have to handle this feature themselves, by tracking route changes in their app and redirecting to the last location in their ApplicationService mount callback.
on syncing the window.history in an iframe
Where I can find a discussion about introducing that approach and why we need to use it?
@restrry It was being explored as an option when figuring out how best to support angular apps in the new platform. We have since decided against it and are moving forward with fixing Angular's URL encoding bugs instead.
I apologize this wasn't recorded anywhere, happened in a flurry of video calls.
Part of our plain entails building out in parallel a few of the pieces (shell AppService, moving Nav APIs to new platform, adding AppService routing). We don't want to merge an incomplete AppService that may confuse other developers.
We've decided to open a branch for us to collaborate on together so we can make sure these pieces fit together well before merging to master. @eliperelman has opened the application-service branch on the root repo.
We may still merge a feature-complete AppService to master before turning it on for routing and bootstrapping of legacy apps.
One snag I've encountered when moving NavLinks to Core is Feature Controls. Feature controls currently rely on server.getUiNavLinks() to populate the initial list of navLinks that should be presented to the user based on their permissions.
This is all done with server-side code in the plugin, which is incompatible with new platform plugins which can be client-only (a hard requirement). Additionally, we may want to allow plugins to conditionally register applications, or register them based on some data fetched from elsewhere, so a static spec solution here will not work (eg. adding a list of applications to kibana.json).
I've talked this over with @kobelb and have come up with a PoC. In order to support Feature Controls and the design goals of the ApplicationService, I propose that:
uiCapabilities (which controls which navLinks are displayed) are not resolved on the server.Capabilities service will make a request to the backend to fetch authorization permissions for the registered applications. The ApplicationService will then consume a filtered list of available Applications based on the user's permissions.In the initial implementation of this, I believe we will have to use an Observable of registered applications since this list is not guaranteed to be "finalized" in any point during the "setup" lifecycle event. Once we have a "start" lifecycle event, we will be able to change this to a single fetch, without the complexities of an Observable.
With the new dependencies between services (ApplicationService, Capabilities, ChromeService) I also plan to move Capabilities to be a sub-component of the ApplicationService. Reason being is that Capabilities depends on ApplicationService's Observable<App[]> and the ApplicationService also needs the list of "available apps" from Capabilities so that it can block routing to an app that the user does not have access to.
Most helpful comment
@eliperelman and I synced today and here is our tentative plan:
_Moved to description above_