# Terminal
rails g controller heavy_tasks
rails g job heavy_tasks
rails g job small_task
rails g stimulus progress-bar
# config/routes.rb
resources :heavy_tasks, only: :create
# app/controllers/heavy_tasks_controller.rb
class HeavyTasksController < ApplicationController
before_action :authenticate_user!
def create
HeavyTaskJob.perform_later(current_user.id)
end
end
# app/jobs/heavy_task_job.rb
class HeavyTaskJob < ApplicationJob
queue_as :default
before_perform :broadcast_initial_update
def perform(current_user_id)
total_count.times do |i|
SmallTaskJob.perform_later(current_user_id, i, total_count)
end
end
private
def broadcast_initial_update
Turbo::StreamsChannel.broadcast_replace_to ["heavy_task_channel", current_user.to_gid_param].join(":"),
target: "heavy_task",
partial: "heavy_tasks/progress",
locals: {
total_count: total_count
}
end
def total_count
@total_count ||= rand(10..100)
end
def current_user
@current_user ||= User.find(self.arguments.first)
end
end
# app/jobs/small_task_job.rb
class SmallTaskJob < ApplicationJob
queue_as :default
def perform(current_user_id, i, total_count)
current_user = User.find(current_user_id)
sleep rand
# Turbo::StreamsChannel.broadcast_replace_to ["heavy_task_channel", current_user.to_gid_param].join(":"),
# target: "heavy_task",
# partial: "heavy_tasks/progress",
# locals: {
# progress: (i + 1) * 100 / total_count
# }
Turbo::StreamsChannel.broadcast_action_to ["heavy_task_channel", current_user.to_gid_param].join(":"),
action: "append",
target: "heavy_task",
content: "<div></div>"
end
end
# app/views/welcome/index.html.erb
<%= render partial: "heavy_tasks/button" %>
# app/views/heavy_tasks/_button.html.erb
<%= turbo_stream_from ["heavy_task_channel", current_user] if user_signed_in? %>
<div id="heavy_task">
<%= button_to "Perform Heavy Task", heavy_tasks_path, class: "btn btn-primary" %>
</div>
# app/views/heavy_tasks/_progress.html.erb
<div id="heavy_task"
data-controller="progress-bar"
data-progress-bar-total-jobs-value="<%= total_count %>">
<div class="progress">
<div class="progress-bar progress-bar-striped active"
role="progressbar"
data-progress-bar-target="progress">
</div>
</div>
# app/javascript/controllers/progress_bar_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="progress-bar"
export default class extends Controller {
static values = {
totalJobs: 0,
completedJobs: 0
}
static targets = ["progress"]
connect() {
this.observer = new MutationObserver((mutationsList, _observer) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
this.increment()
}
}
})
this.observer.observe(this.element, { childList: true })
}
increment() {
this.completedJobsValue++
this.updateProgress()
}
updateProgress() {
let progress = (this.completedJobsValue / this.totalJobsValue) * 100
this.progressTarget.style.width = `${progress}%`
}
}