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.