The project is in a healthy, maintained state
Replaces long-lived ActiveAdmin index downloads with a batched export workspace, customizable views, and export column macros.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

>= 4.0.0.beta13, < 5
>= 7.1
 Project Readme

activeadmin_batched_export

Gem Version Test Status

Batched CSV, JSON, and XML export workspace for ActiveAdmin 4.

Replaces single long-lived index downloads with a workspace page that loads filtered data in sequential batches, then offers one client-side file download.

Requirements

  • Ruby 3.4+
  • Rails 7.1+
  • ActiveAdmin 4.0.0.beta13+
  • importmap-rails and Stimulus (ActiveAdmin 4 default)

Dependencies

Required at runtime:

  • rails (>= 7.1)
  • activeadmin (>= 4.0.0.beta13, < 5)
  • importmap-rails

The host app must load ActiveAdmin with importmap and Stimulus (the ActiveAdmin 4 default). No extra JavaScript build step is required beyond the engine pins.

Installation

Add to your Gemfile:

gem "activeadmin_batched_export"

Then:

bundle install

The engine registers routes, views, importmap pins, and install hooks automatically. Optional host overrides go in config/initializers/activeadmin_batched_export.rb.

To freeze export ids across batches (RFC-0003), create this table in the host app:

create_table :active_admin_batched_export_snapshot_rows do |t|
  t.string :token, null: false
  t.string :resource_type, null: false
  t.bigint :record_id, null: false
  t.integer :position, null: false
  t.datetime :created_at, null: false
end
add_index :active_admin_batched_export_snapshot_rows, [:token, :position], unique: true
add_index :active_admin_batched_export_snapshot_rows, :created_at

Without that table, batches walk a live keyset cursor. With it, the first batch stores filtered primary keys and later batches join live rows in freeze order. The token is not an access grant: rows still load through find_collection.

Enable download formats per resource:

ActiveAdmin.register Order do
  index download_links: [:csv] do
    # ...
  end

  csv do
    column :id
    column :email
  end
end

Configuration

# config/initializers/activeadmin_batched_export.rb
ActiveAdmin::BatchedExport.configure do |config|
  config.batch_size = 1000
  config.max_batch_size = 10_000
  config.large_export_row_threshold = 25_000
  config.max_export_rows = nil
  config.snapshot_ttl = 86_400
  config.stimulus_controller = "activeadmin-batched-export--batched-export"

  config.styles = ActiveAdmin::BatchedExport::Styles.new(
    primary_button: "rounded bg-emerald-600 px-4 py-2 text-white"
  )

  config.filename_proc = lambda do |resource_config, format, controller|
    "#{resource_config.resource_name.plural}-export.#{format}"
  end

  config.register_macro(:company_token, lambda { |value, _record, _column|
    value.to_s[0, 6]
  })
end

Per-resource options

ActiveAdmin.register User do
  batched_export column_selection: true, batch_size: 500,
    includes: [:account],
    macros: {email: :mask_email, phone: :mask_phone}

  csv do
    column :id
    column :email, macro: :mask_email
    column :notes, macro: ->(value, _record) { value.present? ? "[redacted]" : nil }
  end
end

Call batched_export on a resource to enable it. The DSL call itself opts the resource in, so you can wrap it in a condition when enablement should vary. Set batched_export enabled: false only when you need to override a global config.default_enabled = true. Resources without a batched_export call stay off unless default_enabled is true.

Built-in export macros

  • :mask_email
  • :mask_phone
  • :truncate_middle
  • :hash_identifier
  • :redact

Register custom macros globally with config.register_macro or per resource with batched_export macros: { ... }.

Theme customization

Match the export workspace to your ActiveAdmin 4 design system without forking batch or download logic.

Style classes

Pass Tailwind utilities or your design-system classes through the initializer:

# config/initializers/activeadmin_batched_export.rb
ActiveAdmin::BatchedExport.configure do |config|
  config.styles = ActiveAdmin::BatchedExport::Styles.new(
    card: "rounded-lg border border-slate-200 bg-slate-50 p-6",
    primary_button: "btn btn-primary",
    secondary_button: "btn btn-outline",
    cancel_button: "btn btn-ghost",
    back_link: "link link-primary"
  )
end

Override only the keys you need; unset keys keep gem defaults (light and dark Tailwind classes).

Available keys: workspace, card, card_title, table, table_body, table_row, table_header, table_cell, table_cell_mono, hint, column_grid, column_label, column_checkbox, heading, progress_wrap, progress_bar, progress_status_row, error, warning, actions, primary_button, secondary_button, cancel_button, back_link.

Override partials

Copy any partial from the gem into app/views/active_admin/batched_export/ in the host app:

  • workspace.html.erb — page shell; start here for layout changes
  • _summary.html.erb
  • _columns.html.erb
  • _filters.html.erb
  • _progress.html.erb
  • _actions.html.erb — load, cancel, save, and back buttons

Stimulus controller

The engine pins batched_export_controller on ActiveAdmin.importmap. To replace it, pin your own file and set:

ActiveAdmin::BatchedExport.configure do |config|
  config.stimulus_controller = "my-batched-export"
end

Adoption checklist

See examples/custom_theme/README.md for locale overrides and verification steps when adding the gem to another themed admin app.

Stimulus controller and assets

The engine pins controllers/activeadmin_batched_export/batched_export_controller and activeadmin_batched_export/chunk_assembly on ActiveAdmin.importmap only. Include the gem app/assets/controllers and app/assets/javascripts paths in the host importmap cache sweeper when developing locally.

How it works

  1. Index download links route to batched_export instead of synchronous format URLs.
  2. Workspace shows filter context, optional column checkboxes, and batch metadata.
  3. Load export always refetches export_meta JSON for a progress estimate, then walks chunks with export_cursor / X-Batched-Export-Next until that header is absent. When the freeze table exists, the first batch also returns X-Batched-Export-Snapshot and later chunks send export_snapshot with the cursor.
  4. User saves the assembled Blob locally. Cancel keeps an incomplete file when any chunks already arrived.

Batched requests limit server memory per request; the browser still holds the full assembled file before save. Very large exports can exhaust tab memory. Tune batch_size, max_batch_size, and large_export_row_threshold for your data width and row counts. The workspace shows a warning when row count reaches the threshold. Set max_export_rows to refuse a collection larger than that count (over_max on meta, 400 on the first batch).

When the freeze table exists, the first batch stores filtered primary keys in NULL-safe order (RFC-0003). Later batches walk those positions and join live cells. Inserts after freeze are omitted. Updates after freeze appear. Deleted freeze ids are skipped. Without the table, each chunk continues after the last returned sort key and primary key (RFC-0002) and is not a point-in-time snapshot. Custom order_by expressions fall back to primary key descending. NULL sort keys sort last in both directions.

JSON and XML exports use the same column definitions as csv blocks. JSON batches return arrays of row objects; the client concatenates those arrays as text into one array. XML batches return record fragments; the client wraps them in a single <export> root. Shapes differ from ActiveAdmin synchronous JSON/XML downloads.

Authorization follows ActiveAdmin download_links and authorize! on the resource. Disable a format with index download_links: [:csv] (or false to hide exports). Batch endpoints reject formats not listed on the resource index presenter.

Development

From the gem root:

bundle install
bundle exec appraisal install
bundle exec rubocop
make test-javascript
bundle exec polyrun parallel-rspec --workers 5 --merge-failures

Matrixed Rails versions use Appraisal: gemfiles/rails72.gemfile and rails8ruby34.gemfile. Run bundle exec appraisal rspec to execute RSpec in each gemfile context.

Shared agent guidance is managed with pray via Prayfile.

Trunk config lives in .trunk/; CI can run trunk via .github/workflows/_trunk_check.yml. Releases: make release or usr/bin/release.rb.

Contributing

See CONTRIBUTING.md. Bug reports and pull requests are welcome at https://github.com/amkisko/activeadmin_batched_export.rb/issues

Links

License

MIT — see LICENSE.md.

Sponsors

Sponsored by Kisko Labs.

Sponsored by Kisko Labs