Extension system video explainer

I’ve created a short video to explain the concept of an extension system. We’re working on creating an extension system for Microfrontends. The details are still being worked out—this video is really just about the high-level concept. It’s the same sort of idea as exists in the RefApp. The “current architecture” I’m describing is the current architecture of the Microfrontends application.

6 Likes

This is absolutely awesome, @bistenes! What a wonderful way to share an idea. Khan Academy might try and recruit you! :slight_smile:

Would extension slots to have a canonical (universally unique) name and widgets to refer to that slot (“I can go into {slot}“), but not be forced to share the same name? Couldn’t an extension could possibly be designed to go into more than one slot?

You may have just not gotten to this details, but would expect an extension slots to have properties (e.g., the minimum & maximum number of extensions allowed)?

Would the extension manager not only discover extension slots & extensions, but also manage the relationships – i.e., allow an admin to enable/disable extensions for a given slot?

It seems like the imperative / centrally managed approach would be useful for explicitly defining deployments (e.g., like a POM file or package.json) and the discoverable approach (like Spring annotations) would be used when the deployment is run – i.e., you declare which “widgets” to include in your deployment within a configuration and then can turn them on/off through an admin UI to the extension manager. Have I got that right?

Yes, that is the architecture I want to move us toward. The architecture I describe in the video is what @florianrappl implemented—an extension system where extensions and extension slots share the same names. I think the next step is just to break those names up.

You may have just not gotten to this details, but would expect an extension slots to have properties (e.g., the minimum & maximum number of extensions allowed)?

That would be a lot more formality than the existing system supports. It wouldn’t be worth it right now, but if doing that kind of thing becomes more convenient, yes, it might make sense.

Would the extension manager not only discover extension slots & extensions, but also manage the relationships – i.e., allow an admin to enable/disable extensions for a given slot?

Yes!

It seems like the imperative / centrally managed approach would be useful for explicitly defining deployments (e.g., like a POM file or package.json) and the discoverable approach (like Spring annotations) would be used when the deployment is run – i.e., you declare which “widgets” to include in your deployment within a configuration and then can turn them on/off through an admin UI to the extension manager. Have I got that right?

Yyyyyyes, yeah roughly, I think so. But the admin UI will serialize everything to files which you can then version control and deploy. So ultimately the whole thing will be reproducible.

1 Like

Thanks @bistenes for the video and explainer. I had some similar thoughts to @burke. This sounds very similar to the approach taken with extension points and extension in the original OpenMRS JSP UI architecture and in the refapp in appframework.

Presumably it will be up to the “slot” to determine what configuration the extensions can provide into it? What about things like sort order - how would one control the order of widgets in the patient chart in this example? It’s clear from the imperative method as to how it would work, but can you give an example of how you might allow control ordering and positioning with the new extension system if, say, 20 widgets all declare themselves?

Thanks, Mike

2 Likes

Yeah! I think those are really important considerations. If we go with a system for which extensions and slots each have their own names, I think it’s easy to imagine what a configuration system might look like:

Here we have an extension slot named “dashboards” and extensions named “radiography-dash”, “vitals-dash”, etc.

That drawing if from a while ago, so it doesn’t necessarily reflect how I’d actually model the patient chart. But I think it demonstrates the config idea.

I’d also add to that “config API” a key called “config” (so it would be disable, order, add, and config) to which you can pass a configuration object that will be passed through to that specific instance of the extension. So if I had a Line Graph extension attached to multiple extension slots (HIV and Vitals, say), it could be configured differently for each.

(“What about two instances of Line Graph on the same slot, graphing different things?” Good question. Maybe they can be differentiated with suffixes delimited by :? We can get into it more if it’s a major concern, but I think it’s a solvable problem.)

You could even create a “UI builder” experience for configuring this, where you can click to remove extensions, search to add them to slots, and drag them around to reorder:

I don’t know how any of that would work without having separate names for extensions and extension points, though.

I don’t know - I’ve read this more and watched the video a few times, and my gut is that the current imperative system is closer to what we want than having everything automatically discovered and appearing in a given slot without any explicit configuration.

But I’m kind of a newbie to the MFE approach, so I might not be thinking about things from the right angle.

Personally I have been thinking of everything as just components with configuration. Some of those components might have a default set of configurations that they run with if none are explicitly indicated (eg. patient chart widgets might choose to load every widget it finds automatically if no explicit configuration is found).

Most of the widgets will ultimately need some sort of configuration, in order to make them reusable.

I’d think that you’d have (for illustrative purposes):

  1. A “obsGraph” component that can be registered with a name and a given configuration in order to render patient weight over time:
"weightGraph":  {
  component: "obsGraph",
  componentConfig:
    concept:  "WEIGHT (KG)",
    x-axis: {
      label: "Date"
      lowerBound:  0
      upperBound:  100
      ...
  }
}

This component might be something you want to reused in various parts of the application, so having it defined and named allows you to identify it in other configuration.

  1. One or more “dashboard” components that can be registered with names and configuration:
"generalDashboard":  {
  rows: [
    columns: [
       { component: "weightGraph", ... }
    ]
  ]
},
"hivDashboard":  {
  rows: [
    columns: [
       { component: "weightGraph", ... }
    ]
  ]
}

One could also choose to instantiate the weightGraph component inline within a single component if there is no need to reuse it across other components.

  1. This same pattern continues up the stack. Whether we are talking about configuring a low-level widget or the top-level application the same paradigm is followed.

Presumably we could establish an approach where a default configuration is utilized by a given component if none is explicitly provided, and this default configuration might involve auto-discovery if that is what makes the most sense.

But I don’t think a system that is built around auto-discovery over configuration as the primary mechanism is necessarily the right direction.

Mike

Thanks for such a thoughtful and thorough reply. Yes, I think your proposal is also reasonable. It’s more or less the direction we had been going up until the idea of extensions was introduced.

Centrally, this is about the desire that “when I, an implementer, turn on e.g. the HIV module, HIV stuff should appear without me having to change configuration.” We discussed this in a meeting, I don’t remember if you were present. This has been advocated mostly by @florianrappl. I remember @mksrom agreeing. I think @mksd, @dkayiwa, and @ibacher chimed in, but I don’t remember what their positions were. @jdick and I have been ambivalent. All those people can of course speak for themselves; my apologies if I’ve completely misrepresented anyone.

In any case, this is a BA question, a question of implementer UX, treating implementers as users, and I think it ought to be decided as such (cue @grace, @ddesimone, …?). From a technical perspective, either way is fine.

There’s also a couple hybrid systems we could consider. Since these are ultimately implementer UX questions, I’ll elide the technical details (it’s all quite doable).

A) The main implementer UX for adding widgets to the dashboard is the config. In the dev tools config editor, when adding a widget, the component key is a magic dropdown searchy input thing that shows all the available options.

B) The main implementer UX for adding widgets to the dashboard is the UI builder in my drawing above. Unlike the full extension system proposal, widgets don’t magically appear. Nothing magically appears. The config can then look a lot like the one you proposed, since it doesn’t have to support separate “add,” “order”, and “disable” keys:

"generalDashboard":  {
  "extensions": [
    { component: "weightGraph", ... }
  ]
}

Wait, maybe I actually really like option (B)…

Hi @bistenes, thanks for the clarification of this topic.

In the discovery system, it sounds like as soon as we need to configure the extension (a given widget for instance) then we’ll loose the advantage of the auto-discovery, right? I mean if we need to add a layer of config (which I assume we bring in a master configuration - is this what’s called the “root config” btw?) then it looks like we get closer already to the imperative mode.

Auto-discovery seems to fit well for components that are standard, that we know where to put and how to they should behave. But as soon as we need to override that default and bring some config, well then we’ll must do some manual work and have to write that config anyway :confused:

A question: with the auto-discovery, would a given MF be able to register multiple extensions? Or is it one MF = on extension?

Another question: I am assuming that if, in the imperative mode, we don’t explicitly use/configure a component, then it won’t be loaded at all right? I am asking this because we may be a bit encouraged to have big MFs that bring many widgets and use only few, thus maybe loading/fetching some unnecessary components and using resources.

While with the discovery mechanism, we’ll be encouraged to keep the MFs small and package more by features. For instance, one loads an Immunization MF, one gets all the right widgets/screens etc… added at the right place. (again, that’s only valuable if we do not need to override the whole default with a config)

And another one: How does this relate to having the import map automatically generated?

Auto-discovery seems to fit well for components that are standard, that we know where to put and how to they should behave. But as soon as we need to override that default and bring some config, well then we’ll must do some manual work and have to write that config anyway

Yeah, figuring out how to both have reasonable defaults and also easy configurability is the heart of this question. But also involved is the question of what the configuration UX is like, since there are versions that support UI configuration, and versions where it’s all JSON, and there are more and less complex versions of that JSON configuration.

which I assume we bring in a master configuration - is this what’s called the “root config” btw

Sorry, I don’t know what “master” means here. And the root config no longer exists at all. Check out the module-config docs for information about how we do configuration. Note that soon, configuration will be possible entirely through the DevTools UI.

A question: with the auto-discovery, would a given MF be able to register multiple extensions? Or is it one MF = on extension?

A single module/package/MF can support multiple extensions. We could impose a 1:1 rule if we really wanted to for some reason.

Another question: I am assuming that if, in the imperative mode, we don’t explicitly use/configure a component, then it won’t be loaded at all right?

Short answer is yes, if we build it right, then yes.

And another one: How does this relate to having the import map automatically generated?

They’re technically separate proposals. A generated import map would be pieced together by the server based on “installed” MFs. Presumably from there you could override that import map, turn things off, etc. But that’s all at the module level, and extensions is really about the application layer. They are related in that they are sort of complimentary in Florian’s vision—when you turn on or turn off a module, the corresponding things appear on or disappear from the UI.


But I really want to emphasize—the answer to all these technical questions is ultimately “yes, if that’s what we want for implementer UX.” Let’s put aside the technical questions for a minute and try and think, okay, actually, how does a non-developer implementer want this application to work.