-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Kibana Management Section Service #43631
Conversation
Pinging @elastic/kibana-app-arch |
I think we should expose |
Ack. I'll get to this tomorrow! |
order: 20, | ||
async mount(context, params) { | ||
const { renderApp } = await import('./my-section'); | ||
return renderApp(context, params); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I understand the Management App will be written in React and all sections will be written in React, too. Maybe this should receive a React component, instead of unmounting React and mounting it back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about this - tighter integration with react vs staying platform agnostic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tentatively proposed tighter integration with React here, too: #36425 (comment).
Practically speaking, anyone who builds an app will use EUI which is React. So I'm just not sure if it's helpful to design our interfaces without React in mind -- it might even end up hindering consumers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyone who does not want to use React can easily render using any other technology by wrapping it in a 5-line React component:
const AppSection: React.FC = () => {
const ref = useRef();
useLayoutEffect(() => {
if (ref.current) renderWhateverOtherTechnology(ref.current);
}, [ref.current]);
return <div ref={ref} />;
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Counterpoint: Any plugin that wants to register a section without using React would have to bundle React in their plugin just do these 5 lines.
I like the consistency this has with the ApplicationService, personally. It makes it one less thing to learn.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the consistency this has with the ApplicationService, personally. It makes it one less thing to learn.
I agree with @joshdover on this... in all likelihood most people who are using this service are also going to be registering an app of some kind, which has an identical mount
function. So they will already be familiar with this pattern and the couple of lines of code that are required to mount an app.
And registering a management section is really no different than registering an application. It's just an app nested inside of another app. The user clicks a nav link, a route is hit, and the app is loaded up and does whatever it wants.
Viewed that way, it makes sense that the experience would be consistent with what the core ApplicationService is providing. I don't think it would be a very good DX if someone found out that they could build their Kibana app however they want, unless they intend to register a management section, in which case they now have to bundle React with their plugin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Counterpoint: Any plugin that wants to register a section without using React would have to bundle React in their plugin just do these 5 lines.
Counter-counterpoint: React will be provided globally to everyone using Webpack Externals, as you cannot have more than one version of React in a single React tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest we look at this topic from technical perspective—instead of blindly copying what Core does for application mounting and "I like"—let's look at what we gain and lose by re-mounting React.
- This React re-mounting adds unnecessary code: (1) un-mount React; (2) mount it back; (3) add tests for all of that—which needs to be written and then maintained. Then you need to (4) re-attach all your Error Boundaries; (5) re-attach all your React contexts (which I don't think is possible, in a non-trivial cases); (6) all your React Suspense handlers will be probably broken (I'm not aware if it is possible to re-connect them).
- Re-mounting React does not make us less dependent on React: in both cases the Management app is written in React and all the sections are written in React. The only difference is, if we do React re-mounting, we need to add extra code, which makes the API harder to use and maintain (and is slower and breaks React Context and breaks React Error Boundaries and breaks React Suspense).
- Management app and all its sections are written in React, by adding a few % of code that re-mounts React does not make us any less React dependent and those few % of code are not needed in the first place.
- Un-mounting React and then mounting it back breaks React Context. (cc @ppisljar @stacey-gammon)
- For example, if we ever had dynamic theming in EUI, where theme is stored in React context, in Kibana we would not be able to use it, at least in a sensible way, because of this React context breaking. (cc @elastic/kibana-design)
- Similar goes for i18n—i18n context has to be re-attached if you want to use
injectI18n()
HOC. (cc @elastic/kibana-stack-services) - It is relevant not just for theming, but any context providers stop working when React is un-mounted.
- I believe also React Error Boundaries and React Suspense stop working when you re-mount React.
- Remounting React is slower performance-wise than just rendering a React component.
- As mentioned above, React will anyways be bundled globally once for everyone, so a plugin will not need to "bundle React just to render a management section"; and if somebody wants to render a non-React section, it can be achieved by this 5-line function.
You can still have the "handler pattern", but in the idiomatic React way using render-props:
// section/index.tsx
export default () => <div>My Section!</div>;
// section/lazy.tsx
export default React.lazy(() => import('.'));
// plugin.tsx
import MySection from './section/lazy';
management.sections.registerLink('my-section', {
id: 'my-link',
order: 20,
renderTitle: () => <>My Link<sup>beta</sup></>,
renderDescription: () => <p>hello, not used in current UI, but possibly in future</p>,
render: async (context, params) => <MySection context={context} params={params} />,
});
- Above the
mount
function is simply renamed torender
function and returns aReact.ReactNode
. And you can see it uses the standard React code-splitting pattern withReact.lazy
, so the section is loaded dynamically on-demand. - This way also allows you to use
React.Suspense
and gives ability to the section to control what is displayed while the section is being loaded or to suspend switching for some milliseconds from one section to another while the other section is being loaded; so that navigation from one section to another looks seamless to the user. - The above pattern is way more powerful, as you can see
renderTitle
andrenderDescription
easily allow you to render anything in those places. - It is also more powerful because the Management app can call the
render
function with updated (or the same) arguments as many times it wants and React will handle it gracefully by diff-ing the HTML and re-rendering only what needs to be re-rendered (if anything). But the proposedmount
function in this RFC cannot easily be used for re-rendering, first you would need to call the callback it returns to unmountmount()()
which would wipe the whole DOM tree of that section, and only then callmount()
to rebuild all of that section again.
About dependence on React. Providing render prop
{
render: async (context, params) => <MySection context={context} params={params} />;
}
instead of DOM mounting callback
{
mount: async function mount(context, params) {
const { renderApp } = await import('./my-section');
return renderApp(context, params);
}
}
does not make us more dependent on React. In the future, we can still independently remove React from the Management app and independently remove React from the individual sections, regardless if it is render
rendering function or mount
handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before I begin, thanks @streamich for this thorough analysis. Getting all these thoughts down "on paper" makes this go way smoother.
This React re-mounting adds unnecessary code: (1) un-mount React; (2) mount it back; (3) add tests for all of that—which needs to be written and then maintained.
I could be missing something, but from what I can tell the only different code that is added here is calling ReactDOM.render
. I'm skeptical this needs to be tested beyond a simple functional test that makes sure you can access the section (which we should already have).
Then you need to (4) re-attach all your Error Boundaries; (5) re-attach all your React contexts (which I don't think is possible, in a non-trivial cases); (6) all your React Suspense handlers will be probably broken (I'm not aware if it is possible to re-connect them).
I'm not sure I understand what you mean by "re-attach" or why that would break. Yes, each call to the mount
function would involve re-rendering a React tree. If the UI needed to subscribe to Observables (eg. the UI theme) these would be re-set-up on each call, but that should be a trivial operation. Maybe there's more code here than I realize.
It should be noted, that if a value provided in context
needs to change it should be provided as an Observable so the React app can subscribe + update when it changes. We should not be unmounting and re-mounting to handle a context value changing.
I'm not sure why React Suspense handlers would break. Some details on this would help.
Remounting React is slower performance-wise than just rendering a React component.
True, but we only need to do this when navigating to a new section. In that case we're about to render a completely new UI anyways. I don't think there's much performance penalty here but maybe we should experiment and actually test that assumption.
As mentioned above, React will anyways be bundled globally once for everyone, so a plugin will not need to "bundle React just to render a management section"; and if somebody wants to render a non-React section, it can be achieved by this 5-line function.
Good point, but the answer here is still it depends. We plan to provide some out of the box webpack configurations, but it's still entirely likely that 3rd party plugin developers may use a different bundler or config that doesn't know React is already available on the page. In this case, the developer would end up bundling a separate copy of React. Though they could work around this, it'd be nice if we made the right thing easy out of the box.
All this to say, I think Vadim's approach is acceptable, it's just that I prefer making Kibana easy to learn over squeaking out a few milliseconds out of the UI.
Too many times in Kibana's history have we invented multiple interfaces & mechanisms for doing very similar things. This has lead to the proliferation of different solutions to the same problem in our source code. It makes learning Kibana development unnecessarily hard.
We need to make Kibana development exceedingly easy. This is as much an internal strategy as it is an external one. Scaling the Kibana & Solution teams at Elastic and growing the plugin community around us is a huge lever in making Kibana an indispensable tool for thousands of users.
Now, is this one thing going to stop someone from picking up Kibana development? Probably not, but the more inconsistencies we add, the worse it becomes. Death by 1000 cuts has to start somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just thought I'd point out that @streamich is out on leave for a bit which is too bad for the sake of this conversation but good for him.
At risk of speaking without a thorough understanding of all the technical details (which are critically important) - is there a way for us to support framework agnostic and react first ways of doing things? I think we're all in agreement that we expect React to be used the vast majority of the time and our primary reasons for not assuming React usage are -
- We should avoid assuming 3rd party contributors have a particular knowledge base
- We wish to avoid more technology coupling than absolutely necessary
- Tech changes
Counterpoint - IF there aren't any functional differences, only syntactic sugar to providing a react interface, are we trying to over optimize a bootstrap procedure?
IMO - these sort of questions become much simpler when we define and agree upon goals.
|
||
An alternative design would be making everything React-specific and simply requiring consumers of the service to provide a React component to render when a route is hit, or giving them a react-router instance to work with. | ||
|
||
This would require slightly less work for folks using the service as it would eliminate the need for a `mount` function. However, it comes at the cost of forcing folks into a specific rendering framework, which ultimately provides less flexibility. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- How many registries/extension points are extendable by 3rd party plugins? In the sake of consistency, they should follow the same convention.
- Do we have an agreement that non-core API should be library-agnostic? I haven't heard about it so far.
- Could we declare a management section to be a part of an application? As I understand the list of the links is the only something that all management sections have in common. We do not create a separate app for
NavDrawer
, but do for management.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How many registries/extension points are extendable by 3rd party plugins? In the sake of consistency, they should follow the same convention.
Index Management allows other plugins to extend its UI. Here's an example of these extension points being consumed. These extension points are tightly coupled to both React and EUI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These extension points are tightly coupled to both React and EUI.
Some of them are coupled, some use POJO to setup content. Depending on the agreement for management section we can adjust (or not) this API as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it’s possible to adjust this API but it becomes difficult for us (the ES UI team that owns this code) to justify time spent on refactoring without a clear benefit, since we built these extension points for our own use, not use by third parties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no problem with some of these very specific extension points being React. I just see the overall Management app is more generic and I think that should be decoupled from React. I think that the other ES UI extension are very unlikely to be consumed by 3rd parties.
type Unmount = () => Promise<void> | void; | ||
|
||
type ManagementSectionMount = ( | ||
context: AppMountContext, // provided by core.ApplicationService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this is not entirely true from a technical point of view. Contexts have different responsibilities and (in theory) different interfaces.
Anyway it's okay for a while.
650c32a
to
cfea517
Compare
Wow! Thanks everyone for the thoughtful discussion and feedback here. This is all incredibly valuable and exactly why I wanted to have an RFC for this service. One thing that I feel is important to clarify is there is no mandate across all of Kibana to be library-agnostic. Each plugin has a different use case & different audience. Just because it is the way Core Services are structured doesn’t mean it is a hard rule for everyone else. 😄 For some folks building more specific extensions (e.g. ES UI), it may not make sense to be library-agnostic… and that’s totally fine! We need to be comfortable with both approaches co-existing, while also being pragmatic about when is the most appropriate time to adopt one approach vs the other. I had a chance to sync with @mattkime @joshdover and @restrry offline, and we discussed this RFC in depth. Due to the nature of this being a very broad, general service for a wide audience, we think it’s important to still offer a library-agnostic solution. But that doesn’t mean we can't also provide a solution to make usage with React more seamless as Matt suggested. TL;DRWe create a utility in // mount function - default
async mount(context, params) {
const { renderApp } = await import('./my-section');
// non-React users need to write their own `renderApp` function
return renderApp(context, params);
}
// mount function - w/ React helper
async mount(context, params) {
const { MySection } = await import('./components/my-section');
// For React users, we can provide a `mountWithReact` helper via `kibana_react`
return mountWithReact(MySection, context, params);
} I've added more detail around this in an update to the RFC, while also addressing some of the other comments. While the debate on the performance merits of one approach vs the other has been instructive, as far as we can tell there is no severe technical reason to disqualify either strategy [Please chime in if you do not believe this to be the case]. Instead, this came down to some practical points to consider; feel free to read on if you are interested in hearing more about those, otherwise you can stop here and look at the updated document 😄 App Arch & Core ConventionsApp Architecture services operate at various levels of abstraction over Core, and encompass a range of things: everything from widely-shared React components or helpers, to more general shared utilities and interfaces, to browser or server-side services that handle common operations or expose some shared state that apps rely on. So it’s safe to say that we concern ourselves with both React-related and library-agnostic services. The two are not mutually exclusive; they are different use cases and the choice when to use each should depend on the audience. It is important to acknowledge that our services are fundamentally similar to services developed by Platform in that they exist solely for Kibana plugin/app devs as their primary audience, and are designed to fit a wide range of use cases across Kibana. Because of these similarities, the lines between App Arch & Platform are sometimes blurred. In fact, it is possible that some services which are provided by App Arch today may become Core services in the future (perhaps even this one!) As a result, it is important we take Core conventions into consideration when building App Arch services. This isn’t “blind adoption,” but rather having a bias toward starting with the most minimal abstraction possible, and then building it out further from there. This strategy is also not about stylistic preference or being “right” — it is about whatever provides the most consistency for users of the Kibana platform. The approach outlined here strikes a balance between both worlds by providing an experience that is familiar & consistent with Core Services users will already know, while still offering an “opt-in” abstraction to make things simpler for React devs. At this point I'd like to open things up for any final comments so we can get this closed in the coming days -- thoughts & feedback are welcome! |
description: string; | ||
basePath: string; | ||
sectionId: string; | ||
order?: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is not url
field in link anymore https://github.com/elastic/kibana/pull/43631/files#diff-203672eb3ddb17b29129b149130c08b5R224 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think id
will be used for the url
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great! Thanks for putting so much work into this @lukeelmers! I had some suggestions about naming that I feel fairly strongly about and some other questions, but I don't want to block progress on this. Happy to chat about any of my suggestions over Slack or Zoom if you want.
Also, what's the intention behind exposing this API on management.sections
instead of management
? If it's to allow the API to scale in the future, I think we may be prematurely optimizing because management's role can be summed up as "surface arbitrary sections" and it seems to me very unlikely it will grow additional functionality. I'd appreciate the DX of being able to just type management.getSection
instead of having to dig one level deeper.
const mySection = management.sections.register({ | ||
id: 'my-section', | ||
title: 'My Main Section', // display name | ||
description: 'hello', // not used in current UI, but possibly in future |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could I suggest we remove description
from sections and links if they're not being used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
order: 10, | ||
euiIconType: 'iconName', | ||
}); | ||
mySection.registerLink({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hope this doesn't come across as nitpicky, but I think names are particularly important in this situation. In both every day conversation and when looking at UIs, I associate the word "link" with either a URL or an anchor tag that takes me to a URL when clicked. But what we're registering here is really an "app" -- a self-contained module that provides users with functionality mediated by a UI. It also has mounting and unmounting behavior; both are characteristics I associate with plugins and apps from my experience with the New Platform's APIs. Can we change this to registerApp
and navigateToSectionLink
to navigateToApp
to reflect this understanding?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
|
||
start(core, { management }) { | ||
// access all registered sections, filtered based on capabilities | ||
management.sections.getAvailable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not entirely sure how to interpret this example. In a real-world scenario would we assign the value returned by getAvailable
to a variable and act upon it?
Are there examples in the codebase where people have to deal with the problem this method solves? Can we update the "Legacy service" section example with some code demonstrating how we currently solve this problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets add a better example, this should retrieve all sections, which management app then loops over and shows them in ui.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
ReactDOM.render( | ||
( | ||
<KibanaContextProvider services={{ ...context }}> | ||
<Component basename={params.sectionBasePath} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the significance of basename
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its the url base path. This is the name used with the application reg api. I think line 75 does a good job demonstrating but I'm happy to amend it if you have suggestions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Thanks for the explanation! I feel like basePath
is a fairly well understood concept in Kibana. I think of it as "the app's namespace inside of the URI". If we changed basename
to basePath
would that carry the connotations? Just throwing it out here, feel free to disregard.
params: ManagementSectionMountParams, | ||
) => Unmount | Promise<Unmount>; | ||
|
||
interface Link { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Continuing the thought from my other comment, I think App
or ManagementApp
would be more accurate/descriptive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
..., | ||
async mount(context, params) { | ||
const { MySection } = await import('./components/my-section'); | ||
return mountWithReact(MySection, context, params); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nit. I think we can clarify the way mountWithReact
works if we retain the same pattern from the above example on line 105.
async mount(context, params) {
const { MySection } = await import('./components/my-section');
const unmountCallback = mountWithReact(MySection, context, params);
return () => unmountCallback();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
Since it's still going to be a few weeks until implementation works starts on this & Luke is out, I'm going to take out of final comment period until myself or @mattkime has time to respond to these comments. |
094122f
to
157b543
Compare
This RFC needed another round of polish so I took a look at the remaining comments and integrated them as best I know how. Summary of changes:
I think this is ready and aim to merge in 3 days. Please speak up if any changes are needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making those changes @mattkime! Looks great to me.
Summary
Copied over from original discuss issue #43499. I've added some of the folks from that issue as reviewers here.
View Rendered RFC
[skip-ci]