Move "Hardware model" page to React

Hi all,

Ron and I are working on converting the Hardware Model page in Foreman to be fully React.
To make this transition smooth as possible and to understand the challenges, I am creating this topic.

This post will have all the things that we believe are necessary and required to do this including challenges that we are not sure how to tackle.

Design

Hardware Model page component

Hardware Model page Components

The hardware models page contains the following components

  1. Title
  2. Breadcrumbs switcher
  3. Search box - input text box that contains the user search query
  4. Search button - on click filters table (7)
  5. Bookmarks - saved search queries
  6. Create button - move to new model page to add a new model
  7. TableView contains:
    • Table (7) - contains model records
    • Per page select box - sets the number of results per page (8)
    • Results label - contains how many items are displayed in the table out of the total results (9)

Note: strike-through-ed items mean that they already implemented in React

Hardware Model page Behavior

  1. The per page select box - currently changing its value sends a request with the per_page parameter in URI and causes loading the entire page. This behavior will be changed: table (7) will be re-rendered accordingly to per_page without manipulating URI.
  2. The search box
    1. When typing a search query in the search box an ajax request is sent to the auto_complete_search endpoint for auto completion options
    2. Clicking on search a search adding a search parameter to URI which ends up with loading the entire page. This behavior is going to be changed: filtering the entries in Redux and re-rendering the table accordingly
    3. Clicking on one of the bookmarks loads the entire page and sets the search box value to the bookmark’s search query. Again, this behavior will be changed: re-render the search box and the table
  3. Clicking on Delete action displays javascript’s confirmation dialog. Probably this will be replaced by Modal to give a consistent look
  4. Clicking on an item in the table will render the Create Model page discribed below.

Hardware Model page action items

  1. Replace search-box with pf-react
    • pf’s TypeAhead seems like the most suitable component for the job
    • it depends on react-bootstrap-typeahead
    • Because of that, it’s yet not obvious how to set an initial search query which is required when a bookmark is chosen unless we use controlling selections however it makes the search box style different than what people are used.
    • Another way to solve this is (1) to fix this in react-bootstrap-typeahead or (2) to fork it and add our changes or (3) to write a workaround in pf
  2. Replace table, per_page selector and results label with pf-react TableView
    • Use Client Paginated Table with small customization
    • API calls
      • api/v2/models returns a model list containing “name”, “id”, “vendor_clas” and “hardware_model” per item
      • That’s not good enough because hosts count is missing
      • One way to solve this is to do another API call for each model
        • api/v2/hosts?search=model=MODEL_NAME&thin=true
        • Pros - Naive
        • Cons - having many models causes too many API calls. In case of failure the user will have partial resolved data
      • Load all api/v2/hosts and map-reduce the results to get number of hosts with a given model.
        • Pros - avoid the API calls
        • Cons - computation on the client side, response size might be big and contain unnecessary information
      • Ohad suggested to render json format with all the needed data it looks like the right direction
        • Pros - avoid API calls, code reuse
      # app/controllers/architecture_controller.rb
        def index
          respond_to do |format|
           format.html
           format.json {
             # resource_data will get all the architectures with `host_count` and `operating systems`
             render :json => {:results => resource_data()}
           }
          end
        end
      
      # app/views/architectures/index/html.erb
      <div id="architectures"></div>
      <%= mount_react_component("Architectures", "#architectures", results) %>
      
  3. Adding confirmation modal on Delete instead of javascript confirmation dialog
  4. Do more research on GraphQL - how does the HW Model schema look like and how to retrieve this data in the future.

Create/Edit Model page

Create/Edit Components

  1. Breadcrumb bar
  2. Text field with the “Name” label
  3. Text field with the “Hardware Model” label - the hardware model. In the bottom there is descriptive information about the meaning of this field and how to retrieve it using the CLI
  4. Text field with the “Vendor Class” label - model’s vendor class. In the bottom there is descriptive information about the meaning of this field and how to retrieve it using the CLI
  5. TextArea with the “Information” label - useful information about the model. In the bottom there is descriptive information about the meaning of this field
  6. Action buttons - Submit to create Hardware Model and Cancel to go back to the Hardware Models page

Create/Edit Behavior and action items

  1. Validation using redux-form
    • Name(2) can not be empty
  2. On creating new model
    • Submit will send POST to /api/v2/models
    • Fields will be set to empty state
  3. On editing an model
    • Submit will send PUT to /api/v2/models
    • Fields will be set with the model data
  4. Cancel will render the models page
    • should we use react-router/reach-router or render components by state?
2 Likes

Amir told Ron and me about TableView, ConfirmDialog and EmptyState which are used in Katello and will save us time during the process.

Btw the searchbox is implemented in Katello too. The components just need to be transferred to the Foreman.

1 Like

I am highly motivated to see this being implemented with graphql. :slight_smile:
The host count problem is

We already have the retrieval of all models implemented.

This should actually be enough to play with this in the index page.

The way I see it, we also need the following:

  • A mutation to create new models
  • A mutation to edit existing models
  • A field that provides the host count for the model
  • A test for the plural field (e.g. retrieve all models)
  • A totalCount field on the connection

That actually seems doable. I’ll see what I can do to help.

I’d also really like to see this page move to a dedicated react controller and use react-router for the navigation. mount_react_component always feels like you actually want to do it properly. :slight_smile:

2 Likes

Is there a fork of foreman with this branch on it? Or better a WIP PR where for those that wish to follow along at home?

thanks @TimoGoebel! I wonder if in this case it would be enough to simply creating the model retrieval and use the current API for other actions (POST/PUT/DELETE) ? (e.g. in this case, I assume its easy to consume as a REST API too?) I do agree that for more complex objects with many more associations, this makes a lot of sense.

That should definitely be possible, you just have to bear in mind that graphql (or actually relay) recommends using global ids as id’s (instead of the database ids). But it should be trivial to process a global id to find the database id on the client.

I played with this a bit. I have a mutation ready to create models. I just need to add a guard to ensure the user is actually allowed to to that. Creating a mutation for update and delete should not be hard now.
I believe we can implement the host count as a connection (models have hosts) and add the count field to the connection. And we also need the count for pagingation. I need to play with that a little more, but it looks really powerful. In general, we should be there pretty fast.

Here is a sneak peak to show that it’s actually not that much code.

1 Like

Is it a good time to start work towards single search form in Foreman header common across all pages? It would act like search everything (or at least the most important resources like hosts/hostgroups) but still as a context sensitive search - when on hardware model page, offer fields relevant to that page.

I have no idea how to do this technically on SPA, in Rails that would be easier job I think (each page is generated so the context is known). But I like the idea of common search UI API - one search component doing global search and local search.

2 Likes

Big :+1: for global search but IMHO it should be a separate RFC to keep focus.

1 Like

ekohl https://community.theforeman.org/u/ekohl Installer
June 20

Big [image: :+1:] for global search but IMHO it should be a separate RFC
to keep focus.

I agree, it would also need design work too.

I have a wish list around search (including global search, history etc),
but imho it makes more sense to change technology first (e.g. move to a
react implementation) and then improve afterwards.

1 Like

I have one concern regarding the client paginated table - this would mean fetching all the records during the initial load. It is probably not an issue for hardware models as their count is usually low but the same approach could cause problems for other resources such as reports. For this reason, we might need to consider using the server pagination on index pages.

4 Likes

Actually, this is another good reason for graphql. The ApolloJS client supports caching so that switching between pages is pretty fast if the data has been fetched once. Just saying :wink:

  1. I agree with Ondrej, imho it was a mistake in the original doc.
  2. we are going to store the results in redux anyway, do we really need to
    store it again? (don’t forget that non turbolinks hits will reset the
    session anyway…so its a long term value anyway)

Absolutely, reasonable approach I am big fan of actually.

We use server-side paginated tables on Katello redux pages already. It’s worth checking the repositories and subscriptions page.

Speaking about the index page, I was wondering if it makes sense to move the whole “Provisioning setup” menu that gathers multiple index pages to a dashboard-like page that would display all the information at one place.

I am so glad this thread got a lot of attention in the last two days.
Thank you for your feedback and if I could edit the original post, I would have done that.

Here I am going to share the plan to transform Hardware Models page to a React page
Again, any feedback is welcome, this time I will try to reply as soon as possible.

Thanks for you time.

Action items - tasks

Step 1 - Move to React components in the Hardware Model Page

  • Search box (Katello already did something similar here) - medium

    • Replace auto_complete_search with pf-react’s TypeAhead
    • Wired it to Redux
    • Create actions
  • Table View (Katello ref) - easy

    • Move to TableView
    • Wired it to Redux
    • Create actions
  • Pagination (Katello ref 1 and 2) - easy

    • Move to Paginator
    • Redux wiring
    • Implement actions

Step 2 - Create/Edit Hardware Model Page

A good reference can be found here.

Please do not introduce a new type of form strings. They should be consistent between the UI and API. See more here

  • Create the Form as a component - easy
  • Move to redux-form - easy

Step 3 - Move Hardware Model into one React Page

  • Turn Hardware Models page into one component - easy
  • Turn the Create/Edit Model page into one component - easy
  • Composite these two pages into one big component that
    navigates between them using react-router - easy
  • Remove dead code from Rails - easy
    • config/routes.rb
    • app/views/models/
    • app/controllers/models_controller.rb
1 Like

State

In this part I am going to focus on how the state will be stored in Redux

"model_page": {
  "page": number, // the current page
  "per_page": number, // how many entities to display per page
  "total": number, // total number of entities
  "sort_by": string, // what column to sort
  "sort_order": oneOf(["ASC", "DESC"]),
  "results": [{
    "name": string, // a link string if authorized otherwise just the name
    "vendor_class": string,
    "hardware_model": string,
    "hosts_count": string, // a link to hosts with search query 
  }],
  "error": ErrorObject,
  "status": oneOf(["PENDING", "RESOLVED", "ERROR"]),
  "searchQuery": string, // the search/filter query given in the search box
  "searchOptions": array.of.string // options for auto completion
  "searchStatus": oneOf(["PENDING", "RESOLVED", "ERROR"]),
}

Events, Actions and State

Here are three components and their flows from triggering an action to changing the state and rendering the component.

Search Box

  1. When the user type the AUTO_COMPLETE_REQUEST action is triggered.
    This action is doing an async call to
    models/auto_complete_search?search=[query] to get auto completion search
    options.

    {
      type: "AUTO_COMPLETE_REQUEST",
    }
    

    Reducer sets the state:

    {
      search_status: "PENDING"
    }
    
  2. On AUTO_COMPLETE_REQUEST success a new action will be dispatched -
    AUTO_COMPLETE_SUCCESS.

    {
      type: "AUTO_COMPLETE_SUCCESS",
      payload: {
        results: data,
      }
    }
    

    Reducer sets the state:

    {
      searchStatus: "RESOLVED",
      searchOptions: action.payload.results,
    }
    
  3. On AUTO_COMPLETE_REQUEST failure however a new action will be dispatched -
    AUTO_COMPLETE_FAILURE.

    {
      type: "AUTO_COMPLETE_FAILURE",
      payload: {
        error,
      }
    }
    

    Reducer doesn’t change the state at all.

    Note Should think of a way to display auto completion error.

Search Button

  1. On clicking the Search button SEARCH_REQUEST is being dispatched.
    This action does an async call to api/models?search=[query]&page=[page]&per_page=[per_page] to
    retrieve results from the server.

    {
      type: "SEARCH_REQUEST"
    }
    

    The reducer sets the following state:

    {
      status: "PENDING",
      searchQuery: query
    }
    
  2. on SEARCH_REQUEST success the SEARCH_SUCCESS action is dispatched.

    {
      type: "SEARCH_SUCCESS",
      payload: {
        results: data.results,
        per_page: data.per_page,
        page: data.page,
        total: data.total_hits,
        sort_by: data.sort.by,
        sort_order: data.sort.order,
      }
    }
    

    The state will be changed by the reducer in the following way:

    {
      status: "RESOLVED",
      results: action.payload.results,
      per_page: action.payload.per_page,
      page: action.payload.page,
      total: action.payload.total,
      sort_by: action.payload.sort_by,
      sort_order: action.payload.sort_order,
    }
    
  3. on SEARCH_REQUEST failure SEARCH_FAILURE is being dispatched.

    {
      type: "SEARCH_FAILURE",
      payload: {
        error: error,
      }
    }
    

    And this is how the state is modified by the reducer:

    {
      status: "ERROR",
      error: action.payload.error.message,
    }
    

Pagination’s Per Page Selector

  1. On changing pagination per page selector the PER_PAGE_CHANGE action is triggered.
    While it is triggered, a request to the following is being sent
    api/models?search=[query]&per_page=[per_page]&page=[page]

    {
      type: "PER_PAGE_CHANGE"
      payload: {
        per_page: per_page,
      }
    }
    

    Dispatching this action changes the state:

    {
      status: "PENDING",
      per_page: action.payload.per_page
    }
    
  2. on SEARCH_REQUEST success the SEARCH_SUCCESS action is dispatched.

    {
      type: "SEARCH_SUCCESS",
      payload: {
        results: data.results,
        per_page: data.per_page,
        page: data.page,
        total: data.total_hits,
        sort_by: data.sort.by,
        sort_order: data.sort.order,
      }
    }
    

    The state will be changed:

    {
      status: "RESOLVED",
      results: action.payload.results,
      per_page: action.payload.per_page,
      page: action.payload.page,
      total: action.payload.total,
      sort_by: action.payload.sort_by,
      sort_order: action.payload.sort_order,
    }
    
  3. on SEARCH_REQUEST failure SEARCH_FAILURE is being dispatched.

    {
      type: "SEARCH_FAILURE",
      payload: {
        error: error,
      }
    }
    

    The state will look like this:

    {
      status: "ERROR",
      error: action.payload.error,
    }
    

I know we are getting this number from the API call, but I do wonder if we should consider using something such as a reselect?

For reference, all posts in the RFC category can be edited, they default to wiki-style posts. So you probably want to do your next design there :smiley:

1 Like

Sounds good to me :+1: