Ever needed a progress bar for some long-running task in your Rails application? You searched Google and couldn’t find anything that easily integrates with Rails? Well, we created a progress_job gem that helps with that problem.
Progress_job is a gem that builds upon delayed_job to give you a simple progress bar you can use in your views. Create a class with your long-running task inside, update the job in each iteration and have ajax calls that will update the progress bar.
ProgressJob
Progress_job depends on delayed_job, so I will write this tutorial using the delayed_job_active_record gem.
After adding the gem in your Gemfile
gem ’delayed_job_active_record’
gem ’progress_job’
and running bundle install,
$ bundle install
run the progress_job generator to add migrations necessary for progress_job to work.
$ rails generate delayed_job:active_record
$ rails generate progress_job:install
$ rake db:migrate
This will add three columns to the delayed_jobs table:
- progress_stage : string => customizable description for the current progress stage of the task
- progress_current : integer => number representing the current progress value of the task
- progress_max : integer => number representing the maximum progress value of the task
Then you can create a custom class extending ProgressJob::Base which will give you access to some handy methods for manipulating the job.
update_progress(step: 10) # increase the progress_current for step
update_stage(’name of stage’) # change the progress_stage
update_stage_progress(’name of stage’, step: 11) # change progress_stage and increase progress_current for step
update_progress_max(progress_max) # change progress_max
Progress_job also gives you a route from which you can get all the info on a progress_job, and it is located at:
GET /progress-jobs/:job_id
Now all you need is an ajax call which will check the route every few seconds and update the progress bar visible on the screen.
Demo app
# app/jobs/export.rb
class ExportJob < ProgressJob::Base
def initialize(users, progress_max)
super progress_max: progress_max
@users = users
end
def perform
update_stage(’Exporting users’)
csv_string = CSV.generate do |csv|
@users.each do |user|
csv << user.to_csv
update_progress
end
end
File.open(’path/to/export.csv’, ’w’) { |f| f.write(csv_string) }
end
end
# app/controllers/exports_controller.rb
class ExportsController < ApplicationController
def index
end
def export_users
users = User.first(100)
@job = Delayed::Job.enqueue ExportJob.new(users, users.count)
end
end
# app/views/exports/index.html.haml
.export
= link_to ’Export Users’, export_users_path, remote: true, class: ’btn btn-primary btn-lg’
.well{style: ’display:none’}
.row
.col-xs-12
.progress-status.text-primary
.row
.col-xs-12
.progress.progress-striped.active
.progress-bar
.text-primary
0%
= link_to ’View csv’, ’/system/export.csv’, class: ’btn btn-success export-link’, style: ’display:none’
# config/routes.rb
Rails.application.routes.draw do
root to: ’exports#index’
get ’export_users’ => ’exports#export_users’, as: :export_users
end
// app/views/exports/export_users.js.haml
:plain
var interval;
$(’.export .well’).show();
interval = setInterval(function(){
$.ajax({
url: ’/progress-job/’ + #{@job.id},
success: function(job){
var stage, progress;
// If there are errors
if (job.last_error != null) {
$(’.progress-status’).addClass(’text-danger’).text(job.progress_stage);
$(’.progress-bar’).addClass(’progress-bar-danger’);
$(’.progress’).removeClass(’active’);
clearInterval(interval);
}
progress = job.progress_current / job.progress_max * 100;
// In job stage
if (progress.toString() !== ’NaN’){
$(’.progress-status’).text(job.progress_current + ’/’ + job.progress_max);
$(’.progress-bar’).css(’width’, progress + ’%’).text(progress + ’%’);
}
},
error: function(){
// Job is no loger in database which means it finished successfuly
$(’.progress’).removeClass(’active’);
$(’.progress-bar’).css(’width’, ’100%’).text(’100%’);
$(’.progress-status’).text(’Successfully exported!’);
$(’.export-link’).show();
clearInterval(interval);
}
})
},100);