Progress Bar in Rails

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

I’ve created a demo app. You can 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(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);