activeadmin_batched_export
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 installThe 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_atWithout 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
endConfiguration
# 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]
})
endPer-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
endCall 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"
)
endOverride 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"
endAdoption 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
- Index download links route to
batched_exportinstead of synchronous format URLs. - Workspace shows filter context, optional column checkboxes, and batch metadata.
- Load export always refetches
export_metaJSON for a progress estimate, then walks chunks withexport_cursor/X-Batched-Export-Nextuntil that header is absent. When the freeze table exists, the first batch also returnsX-Batched-Export-Snapshotand later chunks sendexport_snapshotwith the cursor. - 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-failuresMatrixed 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
- GitHub
- GitLab
- RubyGems
-
Versions Atom (feed id
11574013576629) - libraries.io
- Deps.dev
- SonarCloud
- Snyk
- Codecov
- OpenSSF Scorecard
License
MIT — see LICENSE.md.
Sponsors
Sponsored by Kisko Labs.