Creating a React UI for foreman_leapp

Hi,
some of you may already know that there has been a new addition to our collection of plugins. foreman_leapp leverages remote execution to perform RHEL upgrades at scale.

I have been working on the UI parts and I’d like to describe some (hopefully interesting) aspects of writing a UI for a plugin with our current js stack.

Quick initial setup

The time from zero to first component was very short. We were able to start quickly because work in other plugins has been already done to pioneer the general way how to do React in a plugin. Core provides functions to register reducers/components and foreman-js supplies the common dependencies, including configs for tests and linting. This will likely improve in the future as the additions for React are merged in foreman_plugin_template.

Mocking core components in tests is a pain

Excellent post by @John_Mitsch already talks about this, so I will not go into details here.

Adding tabs to erb page with Slot & Fill

This turned out to be a bit of a problem:

<ul class="nav nav-tabs" data-tabs="tabs">
  <li class="active">
    <a href="#primary" data-toggle="tab"><%= _('Overview') %></a>
  </li>
  <%= slot('tabHeaderSlot', true) %>
</ul>

will give us

<ul class="nav nav-tabs" data-tabs="tabs">
  <li class="active">
    <a href="#primary" data-toggle="tab">Overview</a>
  </li>
  <div>
    <li>...</li>
  </div>
</ul>

This a result of how React mounts onto page rather than a problem with slot. ReactDOM.render modifies the content of container element but does not modify the container. In other words, it cannot replace the target container element, only modify children, so there is no way to get rid of the superfluous div once we add it as a mount point. Targeting ul does not help as React completely takes over the node content and removes the existing tabs.

This was solved by extending the tabs using pagelets and mounting the tab content into the pagelet partial, so we were able to leverage the existing infrastructure for extending pages through defined extension points rather than using Deface.

Seamless integration with existing pages

Workflow: User selects items in a list of problems detected during leapp preupgrade check which should be fixed. When ‘Fix selected’ button is clicked, user is redirected to new job invocation page, where the form is pre-filled with the data from the selection.

Solution: Because the user selection may contain a lot of items, I will be using POST instead of GET for the redirect so that I do not run into #18264, the button component looks something like the following:

const FixSelectedButton = ({ ids, postUrl, disabled, csrfToken }) => {
  const { hostIds, entryIds } = ids;

  return (
    <form action={postUrl} method="post">
      <Button type="submit" disabled={disabled}>
        {__('Fix Selected')}
      </Button>
      <input type="hidden" name="authenticity_token" value={csrfToken} />
      <input type="hidden" name="feature" value="leapp_remediation_plan" />
      {hostIds.map(hostId => (
        <input type="hidden" name="host_ids[]" key={hostId} value={hostId} />
      ))}
      <input
        type="hidden"
        name="inputs[remediation_ids]"
        value={entryIds.join(',')}
      />
    </form>
  );
};

I am using React to mimic a Rails-style form with hidden fields so that I am able to POST to a Rails controller. This also means I have to manually scoop the CSFR token from the page and pass it into my component - doable, but not great.

Value of data in store

Problem: Job invocation generates a leapp report. To provide a nice user experience, I’d like the job invocation page to fetch the report when the job status changes from running to finished.

Solution: Luckily, one of the rex components on the page uses React and has data about job state in store. That way, I was able to know about job status change and fetch the report when it happens. Without subscribing to the existing data in store, the solution would have been much more involved.

6 Likes

Thanks for this great write up. I’d love to see more of these.

This is a common pattern. I wonder if this could be captured in a generic component.

Awesome write up!

I feel we should fix the csrfToken token asap. Our login form is experiencing issues with the token as well.
In general, I agree it would be great to improve a way how we render rails forms from react to be more user friendly.

The slot&fill needs to be fixed in order to make the whole React thing work as well though :thinking:

1 Like

Awesome write up! Thanks for sharing.

I’m glad this was able to help you. If you use the jest config fix, please let me know how it goes.

I also am wondering if this is something of interest to foreman_leapp? RFC: Don't duplicate npm packages in Katello and plugins for React testing

I think we can standardize the plugin story a bit and 1) not duplicate packages from Foreman in devDependencies 2) use foreman code in plugin testing.

My thought is to create an npm package that uses the logic to “find foreman” during testing and then be able to use that in the jest config. This would be helpful to not duplicate the same logic across plugins. I plan to look further into this when I get a chance, I’ll continue to share progress and would love to get feedback from other plugins as it continues.