Building a dynamic dashboard with Hotwire, part 1

In this series we are building an admin panel, using Hotwire. The admin panel will have multiple tabs and plenty of interactivity. Let’s get started.

What is Hotwire?

Hotwire is a new framework, by Basecamp, which is built into Rails 7. The framework provides an alternative to frontend frameworks like React.

Frontend architecture typically involves a JSON API along with large amounts of frontend JavaScript code. Instead, Hotwire sends HTML fragments from the server, which are rendered on the frontend with minimal JavaScript.

Building an admin dashboard

Here’s a mockup of the admin panel we’re building:

Demo Admin Dashboard Mockup

There’s a row of tabs that switch between various business metrics. Each of the metrics will be updated in real time.

Building the layout

First we’ll build the page layout to roughly match the mockup. We’ll start by adding a route.

1
root 'home#index'

We’ll also need a controller and index action.

1
2
3
4
class HomeController < ApplicationController
  def index
  end
end

Next, we’ll add the HTML layout. The design doesn’t need to be perfect at this stage, it just needs to include the key elements.

1
2
3
4
5
6
7
8
<h1>Demo Admin Dashboard</h1>

<div class="button-row">
  <button>Real time revenue</button>
  <button>Real time orders</button>
</div>

<h2>$104,000</h2>

Lastly, we’ll add a little CSS to center everything:

1
2
3
4
5
6
7
h1, h2 {
  text-align: center;
}

.button-row {
  text-align: center;
}

Making revenue dynamic

In this part 1 post, we’re going to focus on making revenue dynamic. In order to do that we’re going to leverage Turbo Streams, which allows Rails to deliver page changes, as HTML fragments. Whenever a sale is recorded in the database, it should be reflected on the UI, in realtime.

Adding a model to capture sales

Before we can start adding Turbo Streams functionality, we’ll need a Sale model to record sales. Let’s add that using the Rails generator:

1
bin/rails g model Sale amount:integer

For this tutorial Sale just needs an amount field.

We also need a method for calculating the sales total, which will be used to display the revenue on the page. We can do that with a simple class method.

1
2
3
  def self.total
    pluck(:amount).sum
  end

Adding view logic to update revenue

With the model in place, we can go ahead and replace the hardcoded amount. In order to make it dynamic, we can use Turbo’s turbo_stream_from method.

1
2
3
4
<%= turbo_stream_from "revenue" %>
<div id="revenue">
  <%= render partial: 'metrics/revenue', locals: { revenue: @revenue } %>
</div>

turbo_stream_from is used to subscribe to a stream, in this case, a stream called revenue.

On the next line, there is a div that wraps the partial that renders the revenue number. Turbo streams will dynamically re-render this number as long as the message fragment targets revenue. We’ll cover that in the next section.

Lastly, we need to create the partial, under metrics/_revenue.html.erb.

1
2
3
<div class="revenue">
  <%= number_to_currency(revenue) %>
</div>

Broadcasting sales from the model

The last thing we need to do is broadcast messages to the view. Turbo Streams can broadcast directly from models, via callbacks, using broadcast_update_to.

1
after_commit -> { broadcast_update_to "revenue", partial: "metrics/revenue", locals: { revenue: Sale.total }, target: "revenue" }

In this case, we’re broadcasting to the revenue stream, which is the same one we’re subscribed to in the view. We’re also telling broadcast_update_to to render the metrics/revenue partial, as well as passing in the required local variables. Lastly, we’re specifying the target as revenue, which will target the id="revenue" element in the view. This tells Turbo which element to update.

Testing

We can test this system via the Rails console. But first, we need to make sure Redis is running. On a mac you can install it via brew and run it using brew services.

To test, simply run the Rails console and create a new Sale:

1
2
3
$ bin/rails c
Loading development environment (Rails 7.0.2.4)
irb(main):001:0> Sale.create!(amount: 21)

We should see the revenue number updated in realtime.