RFC: Expandable Component Architecture


#1

Hello everyone,

As we all know, plugins need the ability to alter pages on foreman core.
To achieve that, developers mostly use deface, which allows you to change DOM dynamically.

While deface works well with rails views, we should avoid using it on react components.
It changes the real DOM on server rendering, while a react component uses the virtual DOM,
and changes in the real DOM won’t affect the virtual DOM.

Recently, the UI team gather up with some ideas.
We took inspiration from react-slot-fill, which has some similarity to Pagelets

Slot - Fill

<Slot> - Extenable point in a component - used by foreman core.
<Fill>- Fils a slot with a JSX content - used by plugins.

A slot will use an internal registry, that globally manages the extendable points.

A fill component will have two filling options:

  1. Replacing - replace the entire slot’s content.
  2. Appending - append content to a slot, therefore plugins can add content to the same slot.

Example

A plugin appends a button in the Notification Drawer's footer:

<NotificationDrawer> 
#content...
<Slot id='notification-drawer-footer' appendable />
</NotificationDrawer>

Plugin:

<Fill target='notification-drawer-footer`>
  <Button> Appended button ! </Button>
</Fill>   

This architecture allows foreman core to be more opinionated with extending pages.
Which personally I guess it is a great advantage from core perspective.
However it does reduce plugins ability to do whatever they want on existing pages / components,
therefore it would be a challenge.


#2

Thanks for filling the big gap in functionality. It sounds like a good solution and I don’t see it as a big downside that plugins are limited. That’s similar to pagelets and I haven’t heard people negative about that. On the contrary, authors appreciated knowing exactly where changes would end up (granted, I’ve spoken to a small number of authors). Perhaps we’ll get closer to a formal plugin API which would make writing plugins that don’t break up upgrades easier.


#3

That is a great change, and I am all for it. One feature that is present in pagelets and probably should be here as well is the order / weight of the <Fill> element. Otherwise the UI can behave unexpectedly.


#4

Thanks for bringing it up @amirfefer. I like the idea of Slot/Fill.

Here are some proposals to make plugin developers life easier:

  1. Abstract the target of the Fill component so plugins can consume an easier version of it from the extendable component as <NotificationDrawer.AppendFooter>...</NotificationDrawer.AppendFooter>.

In foreman core:

const fillTargetFactory = (target) => ({ children }) => (
  <Fill target={target}>
    {children}
  </Fill>
);

const NotificationDrawer = props => (
  <div>
    #content...
    <Slot id='notification-drawer-footer' appendable />
  </div>
);

NotificationDrawer.AppendFooter = fillTargetFactory('notification-drawer-footer');

export default NotificationDrawer;

In Plugin:

import { NotificationDrawer } from 'foremanReact'; 

const MyComponent = () => (
  <div>
    #content...
    <NotificationDrawer.AppendFooter>
      <Button> Appended button ! </Button>
    </NotificationDrawer.AppendFooter>
  </div>
);

This way plugins won’t care much about the target and they can rely on the NotificationDrwer Itself.

  1. If we want it to be easy to use it should also be easy to research how to do stuff.
    I think we should have some sort of documentation where you can find what you can extend and how (In the storybook?).
    We can achieve it by creating automation that can read the code, search all the uses of fillTargetFactory and generate those docs from it.

#5

Just a brief reply. I think it’s a good idea to have well defined extension points. Please note, that not all plugins use deface. Some common extension points are also available via prepending a view helper method.

The plugins still need to change from erb to jsx if a change is made in core. If we change one page at a time from ERB to JSX, plugins have to do the change immediately after the change in core. That might be a challenge for some plugin author. I’m wondering if it would be easier to just release foreman 2.0 that has no erb at all so plugins then just need to adjust once. What do you think?


#6

I think it’s a good point @TimoGoebel, it makes sense to me because it’s a breaking changes.


#7

This sounds as something bigger to discuss probably out of this thread. It means drop the whole UI and build it from scratch, do I read the proposal correctly?


#8

I like the concept. Also I want to +1 what Shim mentioned, we need to have control over the order, think of bulk actions on hosts list page.


#9

:+1: to having more structure around what plugins can extend. I think that this provides the best experience for our users because it ensures that the pages will be displayed as we intended.

As far as how to go about doing this, I think first we need some data about how many plugins there are that are using deface and what they are using it for and where. I think this could inform our decision as to how to approach this.


#10

A quick grep tells me at least:


#11

As @TimoGoebel said, many plugins are also extending Foreman’s rails helpers to add functionality in the UI, if these helpers are replaced with react components those use cases will also needed to be taken into account


#12

I agree, but it wasn’t a proposal. I was simply asking how we should be doing the transition.


#13

Just in case it was not clear for random readers - Foreman does have a concept of Pagelets in its MVC codebase and plugins use this extensively. We used to have a situation when most plugins relied deface, those times are gone luckily.

We’ve learned a lesson from Pagelets: we need a good mechanism for other plugins to offer new “extension points” easily. Include this in your design if that’s not obvious from the day one.


#14

react-slot-fill looks promising. We need to think about how to do ordering of extensions as @Shimon_Shtein mentioned.

One place where we can’t use this approach is tables. At the moment table columns are defined as a data structure that is passed to a table component via props. We use reactabular for that, see the docs for more details. We need to find a way to modify existing column components (slot-fill probably covers that) but also for adding new columns (slot-fill won’t help here).