bogdan/datagrid

Bogdan Gusiev

October 2013

App Support pages

Why?

select * from users where ...

Because I want to know

what is in my database

If you still don't care - your project is not in production yet

Project values

  • Team
  • Happy users
  • Data
  • Code

What is in my database?

  • Find data
  • Browse data
  • Edit data
  • CSV export

A perfect data finding tool

A perfect data finding tool

Not a big deal to use ActiveRecord

Reward.by_status(status).
  by_reason(reason).
  where(:created_at => from_created_at..to_created_at).
  by_campaigns(selected_campaigns)

Some problems

User.where(:created_at => Date.today)
User.where(:created_at => Date.today.beginning_of_day..Date.today.end_of_day)
User.where(:created_at => -Infinity..1.month.ago.end_of_day)
def convert_date_to_timestamp(value)
  if !value
    value
  elsif value.is_a?(Array)
    [value.first.try(:beginning_of_day), value.last.try(:end_of_day)]
  elsif value.is_a?(Range)
    (value.first.beginning_of_day..value.last.end_of_day)
  else
    value.beginning_of_day..value.end_of_day
  end

Data table

%table
  %tr
    %th Id
    %th Email
    %th Name
    %th Creation Date
  - @users.each do |user|
    %tr
      %td= user.id
      %td= user.email
      %td= user.name
      %td= user.created_at.to_date

It is easy to build

column(:id)
column(:email)
column(:name)
column(:created_at) do |model|
  model.created_at.to_date
end

But what about:

  • Ordering
  • CSV export
  • Columns visibility

But the main problem is GUI

I would spend 90% of time building GUI

If I would use ActiveRecord

So, let the GUI autogenerate itself

The Basics

  • Scope
    scope { User.includes(:profile).order("created_at desc") }
  • Filters
    filter(:email, :string, options) do |value|
      where("email ilike '%#{value}%'")
    end
    
  • Columns
    column(:created_at) do |user|
      user.created_at.to_date
    end
    
filter(:created_at, :date, :range => true, :default => (1.month.ago.to_date..Date.today))
filter(:campaign_type, :enum, :select => CAMPAIGN_TYPES) do |value|
  where(:type => value)
end
filter(:tag_name, :enum, :select => :tag_names) do |value|
  by_tag_name(value)
end

filter(:active, :eboolean) do |value|
  value == "YES" ? active : inactive
end
column(:related_email, mandatory: true) do |offer|
  offer.person.email
end

column(:short_link, mandatory: true) do |offer|
  format(offer.short_url) do |value|
    link_to("Claim", value)
  end
end

column(:campaign, mandatory: true) do |offer|
  campaign = offer.campaign
  format(campaign.name) do |value|
    link_to(value, site_campaign_path(campaign.site, campaign))
  end
end

%w(share click visit referral).each do |action|
  column(action, header: action.pluralize.humanize) do |offer|
    offer.activities.by_actions(action).count
  end
end

There is no GUI problems

class UsersController
  def index
    @grid = UsersGrid.new(params[:users_grid])
    @assets = @grid.assets.page(params[:page])
  end
end
= datagrid_form_for @grid, :url => report_path
= datagrid_table @grid, @assets

Basic Filter Types

  • default
  • date
  • enum
  • boolean
  • eboolean
  • integer
  • float
  • string

Basic Filter options

  • multiple
  • range
  • allow_blank
  • select
  • header
  • default

Column options

  • mandatory
  • order
  • order_desc
  • order_by_value

The Power

Changing scope on the fly

grid = ProjectsGrid.new(params[:my_grid]) do |scope|
  scope.where(:owner_id => current_user.id)
end

Define asc/desc ordering

column(
  :priority, 
  # suppose that models with null priority will be always on bottom
  :order => "priority is not null desc, priority", 
  :order_desc => "prioritty is not null desc, priority desc"
)

Different HTML/CSV formatting

column(:name) do |asset|
  format(asset.name) do |value|
    content_tag(:strong, value)
  end
end

Order by joined column

column(:profile_updated_at, :order => proc { |scope|
  scope.join(:profile).order("profiles.updated_at")
}) do |model|
  model.profile.updated_at.to_date
end 

Infinite ranges and default values

filter(:posts_count, :integer, :range => true, :default => [1, nil])

Multivalues filter

filter(:id, :integer, :multiple => true)
Grid.new(:id => "1,2").assets # => select * from <table> where id in (1,2)

GUI flexibility

rake datagrid:copy_partials
app/views/datagrid
├── _form.html.erb
├── _head.html.erb
├── _order_for.html.erb
├── _row.html.erb
└── _table.html.erb

App Specific options

column(:new_sales, :tooltip => "Amount of sales comming from referral programs")

app/views/datagrid/_head.html.erb

 
               <th class="<%= datagrid_column_classes(grid, column) %>">
                 <%= column.header %>
          +      <% if text = column.options[:tooltip] %>
          +        
          +      <% end %>
                 <%= datagrid_order_for(grid, column) if column.order && options[:order]%>
               </th>

Admin pages gems

  • activeadmin
  • rails_admin
  • active_scaffold
  • etc.

Data filtering component is very basic

Possible to use datagrid instead

But maintainers refuse to support it out of the box

class DatagridActiveAdmin
  def self.integrate_datagrid(context, grid_class)
    context.config.filters = false
    context.config.paginate = false
    context.send :collection_action, :index  do
      datagrid = grid_class.new(params[grid_class.param_name])
      respond_to do |f|
        f.html do
          render template: "admin/datagrid", locals: {datagrid: datagrid}, layout: 'active_admin'
        end
        f.csv do
          send_data(@grid.to_csv, type: 'text/csv', 
            filename: "#{datagrid.class.to_s.underscore}-#{DateTime.now}.csv")
        end
      end
    end
  end
end
ActiveAdmin.register User do
  DatagridActiveAdmin.integrate_datagrid(self, UsersGrid)
end

Pro Development Tips

Human readable is not enough to be good DSL

People don't like learning APIs. It is good learn less and do more.

# Bad DSL
2+2
# Good DSL
2.add :to => 2

Dont rewrite Ruby features as custom DSL

column(:created_at, format: :date)

Localization? Format? Timestamp?

module DatagridExtension
  def date_column(name)
    column(name) do |model, grid|
      I18n.localize(model.send(name).to_date, :locale => grid.current_locale)
    end
  end
end

Be Object Oriented

class UsersGrid
  include MyProjectDatagrid
  def current_locale
describe User do
  context do
    before do
      puts self.inspect
    end
  end
end
#<RSpec::Core::ExampleGroup::Nested_1:0x007fcc550a40a0 ... >

Entry level

=

number of API methods

It is really hard to maintain a documentation
for 30+ methods with 200+ options.

It is even more hard
to get someone else familiar with them.

Quick Start

VS

Flexibility


Try to cover both

but have one of them preferred

Code

github.com/bogdan/datagrid

Docs

github.com/bogdan/datagrid/wiki

Demo

datagrid.herokuapp.com