Progress Bar in Rails

progress-bar-in-rails-0

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.

title

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

I’ve created a demo app. You can take a look at the demo or view its source.

# 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(&#8217;Exporting users&#8217;)
    csv_string = CSV.generate do |csv|
      @users.each do |user|
        csv << user.to_csv
        update_progress
      end
    end
    File.open(&#8217;path/to/export.csv&#8217;, &#8217;w&#8217;) { |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 &#8217;Export Users&#8217;, export_users_path, remote: true, class: &#8217;btn btn-primary btn-lg&#8217;
  .well{style: &#8217;display:none&#8217;}
    .row
      .col-xs-12
        .progress-status.text-primary
    .row
      .col-xs-12
        .progress.progress-striped.active
          .progress-bar
            .text-primary
              0%
  = link_to &#8217;View csv&#8217;, &#8217;/system/export.csv&#8217;, class: &#8217;btn btn-success export-link&#8217;, style: &#8217;display:none&#8217;

# config/routes.rb
Rails.application.routes.draw do
  root to: &#8217;exports#index&#8217;
  get &#8217;export_users&#8217; => &#8217;exports#export_users&#8217;, as: :export_users
end

// app/views/exports/export_users.js.haml
:plain
  var interval;
  $(&#8217;.export .well&#8217;).show();
  interval = setInterval(function(){
    $.ajax({
      url: &#8217;/progress-job/&#8217; + #{@job.id},
      success: function(job){
        var stage, progress;

        // If there are errors
        if (job.last_error != null) {
          $(&#8217;.progress-status&#8217;).addClass(&#8217;text-danger&#8217;).text(job.progress_stage);
          $(&#8217;.progress-bar&#8217;).addClass(&#8217;progress-bar-danger&#8217;);
          $(&#8217;.progress&#8217;).removeClass(&#8217;active&#8217;);
          clearInterval(interval);
        }

        progress = job.progress_current / job.progress_max * 100;
        // In job stage
        if (progress.toString() !== &#8217;NaN&#8217;){
          $(&#8217;.progress-status&#8217;).text(job.progress_current + &#8217;/&#8217; + job.progress_max);
          $(&#8217;.progress-bar&#8217;).css(&#8217;width&#8217;, progress + &#8217;%&#8217;).text(progress + &#8217;%&#8217;);
        }
      },
      error: function(){
        // Job is no loger in database which means it finished successfuly
        $(&#8217;.progress&#8217;).removeClass(&#8217;active&#8217;);
        $(&#8217;.progress-bar&#8217;).css(&#8217;width&#8217;, &#8217;100%&#8217;).text(&#8217;100%&#8217;);
        $(&#8217;.progress-status&#8217;).text(&#8217;Successfully exported!&#8217;);
        $(&#8217;.export-link&#8217;).show();
        clearInterval(interval);
      }
    })
  },100);