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(’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);