activeadmin_batched_export 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +38 -0
- data/CONTRIBUTING.md +29 -0
- data/GOVERNANCE.md +32 -0
- data/LICENSE.md +21 -0
- data/README.md +208 -0
- data/SECURITY.md +28 -0
- data/activeadmin_batched_export.gemspec +67 -0
- data/app/assets/controllers/activeadmin_batched_export/batched_export_controller.js +188 -0
- data/app/views/active_admin/batched_export/_actions.html.erb +25 -0
- data/app/views/active_admin/batched_export/_columns.html.erb +21 -0
- data/app/views/active_admin/batched_export/_filters.html.erb +11 -0
- data/app/views/active_admin/batched_export/_progress.html.erb +14 -0
- data/app/views/active_admin/batched_export/_summary.html.erb +40 -0
- data/app/views/active_admin/batched_export/workspace.html.erb +22 -0
- data/app/views/active_admin/shared/_download_format_links.html.erb +7 -0
- data/config/locales/activeadmin_batched_export.en.yml +29 -0
- data/config/polyrun_coverage.yml +7 -0
- data/lib/activeadmin/batched_export/configuration.rb +37 -0
- data/lib/activeadmin/batched_export/controller_methods.rb +276 -0
- data/lib/activeadmin/batched_export/engine.rb +45 -0
- data/lib/activeadmin/batched_export/export_macro_catalog.rb +77 -0
- data/lib/activeadmin/batched_export/export_macro_resolver.rb +73 -0
- data/lib/activeadmin/batched_export/install.rb +33 -0
- data/lib/activeadmin/batched_export/resource_extension.rb +65 -0
- data/lib/activeadmin/batched_export/styles.rb +45 -0
- data/lib/activeadmin/batched_export/version.rb +7 -0
- data/lib/activeadmin/batched_export.rb +25 -0
- data/lib/activeadmin_batched_export.rb +5 -0
- data/sig/activeadmin/batched_export.rbs +5 -0
- metadata +377 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: eaf6853f6364e953e0122673e0f9721fa64ce10fb73decd82b61e27c5998bdac
|
|
4
|
+
data.tar.gz: 030b4c107bd0600d8b73d0dfadaa287e1685d49ee3253f918a6b1a735be0ccec
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ddd6b51ab26880726f33a4ad7e82bc7c347035207c220464f3f91f9a808deee90f62b9d68cce545ac79dfa2dbe1437b7fcd0d60dacdd0bd1938d71fa0d69840b
|
|
7
|
+
data.tar.gz: 4e4c937bd470a80b9c657a72361e4bb5f257b558d4c80f0edb319e0a76f9bb0808025bff8ca8c71256b05182cb7ea2548066ab77409fdffe53a47596b57da3ac
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-07-10)
|
|
4
|
+
|
|
5
|
+
- Initial public release of batched ActiveAdmin export workspace.
|
|
6
|
+
- Batched CSV, JSON, and XML export with Stimulus client assembly.
|
|
7
|
+
- Configurable styles, batch size, filename proc, and per-resource `batched_export` DSL.
|
|
8
|
+
- Export column macros: named catalog, built-ins, and Ruby proc support.
|
|
9
|
+
- Overridable view partials and download link routing.
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We pledge to make participation in our community a harassment-free experience for everyone. We operate on principles of mutual respect, privacy, and authentic engagement. We value substantive contributions and clarity on intentions.
|
|
6
|
+
|
|
7
|
+
## Our Standards
|
|
8
|
+
|
|
9
|
+
Examples of behavior that contributes to a positive environment include:
|
|
10
|
+
|
|
11
|
+
* **Authenticity:** Engaging with genuine curiosity and admitting uncertainty rather than feigning knowledge.
|
|
12
|
+
|
|
13
|
+
* **Responsible Innovation:** Taking full responsibility for any content or code contributed, whether manually written or generated by automation tools.
|
|
14
|
+
|
|
15
|
+
* **Gentle Correction:** Responding politely to errors. We view mistakes as opportunities for learning, provided they are addressed with humility.
|
|
16
|
+
|
|
17
|
+
* **Inclusive Language:** Using language that welcomes diverse perspectives and respects the privacy and identity of all participants.
|
|
18
|
+
|
|
19
|
+
Examples of unacceptable behavior include:
|
|
20
|
+
|
|
21
|
+
* **Harassment:** Public or private harassment, trolling, or insulting comments.
|
|
22
|
+
|
|
23
|
+
* **Weaponized Complexity:** Using jargon or overwhelming volume (including automated spam) to silence others.
|
|
24
|
+
|
|
25
|
+
* **Publishing Private Information:** Sharing others' data or personal context without explicit permission.
|
|
26
|
+
|
|
27
|
+
## Artificial Intelligence & Automation
|
|
28
|
+
|
|
29
|
+
In accordance with our commitment to **Collective Awareness**:
|
|
30
|
+
|
|
31
|
+
* Contributors are responsible for the accuracy and security of any AI-generated artifacts they submit.
|
|
32
|
+
|
|
33
|
+
* "The AI wrote it" is not a valid excuse for introducing bugs, security vulnerabilities, or bias.
|
|
34
|
+
|
|
35
|
+
## Enforcement
|
|
36
|
+
|
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@kiskolabs.com. All complaints will be reviewed and investigated promptly and fairly.
|
|
38
|
+
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Contributing Guidelines
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to activeadmin_batched_export. We value **learning over perfection** but require **rigor and responsibility**.
|
|
4
|
+
|
|
5
|
+
## The Golden Rule of Automation
|
|
6
|
+
|
|
7
|
+
We welcome the use of AI and automation tools to reduce toil, but you must strictly adhere to the following:
|
|
8
|
+
|
|
9
|
+
1. **You are the Author:** You act as the responsible agent for any code you submit. You must review, debug, and understand every line.
|
|
10
|
+
2. **Manage Cognitive Load:** Do not submit massive, unreviewed automated dumps. Respect the reviewers' time by annotating complex logic.
|
|
11
|
+
3. **Security:** Never feed project secrets or private context into public AI models.
|
|
12
|
+
|
|
13
|
+
## How to Contribute
|
|
14
|
+
|
|
15
|
+
### 1. Reporting Issues
|
|
16
|
+
|
|
17
|
+
- **Verify Accuracy:** Before posting, verify your information. Avoid generalizations.
|
|
18
|
+
- **Use Structured Inputs:** Use our Issue Templates to provide clear goals, constraints, and reproduction steps.
|
|
19
|
+
|
|
20
|
+
### 2. Pull Request Process
|
|
21
|
+
|
|
22
|
+
- **Scope:** Keep PRs focused on a single goal.
|
|
23
|
+
- **Context:** Explain why the change is necessary.
|
|
24
|
+
- **Testing:** Run `make test` locally before opening a PR.
|
|
25
|
+
|
|
26
|
+
### 3. Review Process
|
|
27
|
+
|
|
28
|
+
- We encourage productive friction. Expect questions about your approach.
|
|
29
|
+
- If a reviewer suggests a change, view it as mutual aid, not criticism.
|
data/GOVERNANCE.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Project Governance
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This project balances **individual autonomy** with **collective coordination**. We aim for a workflow that supports rapid iteration while maintaining strict guardrails for safety and architectural integrity.
|
|
6
|
+
|
|
7
|
+
## Roles
|
|
8
|
+
|
|
9
|
+
### Contributors
|
|
10
|
+
|
|
11
|
+
Anyone who submits code, documentation, or participates in discussions. Contributors are expected to exercise **Direct Action**—taking ownership of problems they identify—while adhering to our quality standards.
|
|
12
|
+
|
|
13
|
+
### Maintainers (Human Oversight)
|
|
14
|
+
|
|
15
|
+
Maintainers are responsible for:
|
|
16
|
+
|
|
17
|
+
1. **Strategic Judgment:** Defining scope and architectural direction.
|
|
18
|
+
|
|
19
|
+
2. **Review:** verifying that contributions (human or automated) meet security and logic standards.
|
|
20
|
+
|
|
21
|
+
3. **Consensus Building:** Facilitating decisions when the community is divided.
|
|
22
|
+
|
|
23
|
+
## Decision Making process
|
|
24
|
+
|
|
25
|
+
### Lazy Consensus
|
|
26
|
+
|
|
27
|
+
For most routine changes, we operate on "lazy consensus." If a proposal is made and no objections are raised within two weeks, it is considered approved.
|
|
28
|
+
|
|
29
|
+
### Strategic Alignment
|
|
30
|
+
|
|
31
|
+
Major architectural changes, high-risk automation integrations, or changes that affect business logic require explicit approval from the Maintainers. We prioritize **substance over performance**—a change must solve a real problem, not just appear polished.
|
|
32
|
+
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 amkisko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# activeadmin_batched_export
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/activeadmin_batched_export) [](https://github.com/amkisko/activeadmin_batched_export.rb/actions/workflows/test.yml)
|
|
4
|
+
|
|
5
|
+
Batched CSV, JSON, and XML export workspace for ActiveAdmin 4.
|
|
6
|
+
|
|
7
|
+
Replaces single long-lived index downloads with a workspace page that loads filtered data in sequential HTTP batches, then offers one client-side file download.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Ruby 3.2+
|
|
12
|
+
- Rails 7.1+
|
|
13
|
+
- ActiveAdmin 4.0.0.beta13+
|
|
14
|
+
- importmap-rails and Stimulus (ActiveAdmin 4 default)
|
|
15
|
+
|
|
16
|
+
## Dependencies
|
|
17
|
+
|
|
18
|
+
Required at runtime:
|
|
19
|
+
|
|
20
|
+
- `rails` (>= 7.1)
|
|
21
|
+
- `activeadmin` (>= 4.0.0.beta13, < 5)
|
|
22
|
+
- `importmap-rails`
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Add to your Gemfile:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
gem "activeadmin_batched_export"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bundle install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The engine registers routes, views, importmap pins, and install hooks automatically. Optional host overrides go in `config/initializers/activeadmin_batched_export.rb`.
|
|
41
|
+
|
|
42
|
+
Enable download formats per resource:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
ActiveAdmin.register Order do
|
|
46
|
+
index download_links: [:csv] do
|
|
47
|
+
# ...
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
csv do
|
|
51
|
+
column :id
|
|
52
|
+
column :email
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# config/initializers/activeadmin_batched_export.rb
|
|
61
|
+
ActiveAdmin::BatchedExport.configure do |config|
|
|
62
|
+
config.batch_size = 1000
|
|
63
|
+
config.max_batch_size = 10_000
|
|
64
|
+
config.large_export_row_threshold = 25_000
|
|
65
|
+
config.stimulus_controller = "activeadmin-batched-export--batched-export"
|
|
66
|
+
|
|
67
|
+
config.styles = ActiveAdmin::BatchedExport::Styles.new(
|
|
68
|
+
primary_button: "rounded bg-emerald-600 px-4 py-2 text-white"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
config.filename_proc = lambda do |resource_config, format, controller|
|
|
72
|
+
"#{resource_config.resource_name.plural}-export.#{format}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
config.register_macro(:company_token, lambda { |value, _record, _column|
|
|
76
|
+
value.to_s[0, 6]
|
|
77
|
+
})
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Per-resource options
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
ActiveAdmin.register User do
|
|
85
|
+
batched_export enabled: true, column_selection: true, batch_size: 500,
|
|
86
|
+
includes: [:account],
|
|
87
|
+
macros: {email: :mask_email, phone: :mask_phone}
|
|
88
|
+
|
|
89
|
+
csv do
|
|
90
|
+
column :id
|
|
91
|
+
column :email, macro: :mask_email
|
|
92
|
+
column :notes, macro: ->(value, _record) { value.present? ? "[redacted]" : nil }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Set `batched_export enabled: false` to skip batched export for a resource.
|
|
98
|
+
|
|
99
|
+
## Built-in export macros
|
|
100
|
+
|
|
101
|
+
- `:mask_email`
|
|
102
|
+
- `:mask_phone`
|
|
103
|
+
- `:truncate_middle`
|
|
104
|
+
- `:hash_identifier`
|
|
105
|
+
- `:redact`
|
|
106
|
+
|
|
107
|
+
Register custom macros globally with `config.register_macro` or per resource with `batched_export macros: { ... }`.
|
|
108
|
+
|
|
109
|
+
## Theme customization
|
|
110
|
+
|
|
111
|
+
Match the export workspace to your ActiveAdmin 4 design system without forking batch or download logic.
|
|
112
|
+
|
|
113
|
+
### Style classes
|
|
114
|
+
|
|
115
|
+
Pass Tailwind utilities or your design-system classes through the initializer:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# config/initializers/activeadmin_batched_export.rb
|
|
119
|
+
ActiveAdmin::BatchedExport.configure do |config|
|
|
120
|
+
config.styles = ActiveAdmin::BatchedExport::Styles.new(
|
|
121
|
+
card: "rounded-lg border border-slate-200 bg-slate-50 p-6",
|
|
122
|
+
primary_button: "btn btn-primary",
|
|
123
|
+
secondary_button: "btn btn-outline",
|
|
124
|
+
back_link: "link link-primary"
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Override only the keys you need; unset keys keep gem defaults (light and dark Tailwind classes).
|
|
130
|
+
|
|
131
|
+
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`, `back_link`.
|
|
132
|
+
|
|
133
|
+
### Override partials
|
|
134
|
+
|
|
135
|
+
Copy any partial from the gem into `app/views/active_admin/batched_export/` in the host app:
|
|
136
|
+
|
|
137
|
+
- `workspace.html.erb` — page shell; start here for layout changes
|
|
138
|
+
- `_summary.html.erb`
|
|
139
|
+
- `_columns.html.erb`
|
|
140
|
+
- `_filters.html.erb`
|
|
141
|
+
- `_progress.html.erb`
|
|
142
|
+
- `_actions.html.erb` — export and back buttons
|
|
143
|
+
|
|
144
|
+
### Stimulus controller
|
|
145
|
+
|
|
146
|
+
The engine pins `batched_export_controller` on `ActiveAdmin.importmap`. To replace it, pin your own file and set:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
ActiveAdmin::BatchedExport.configure do |config|
|
|
150
|
+
config.stimulus_controller = "my-batched-export"
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Adoption checklist
|
|
155
|
+
|
|
156
|
+
See [examples/custom_theme/README.md](examples/custom_theme/README.md) for locale overrides and verification steps when adding the gem to another themed admin app.
|
|
157
|
+
|
|
158
|
+
## Stimulus controller and assets
|
|
159
|
+
|
|
160
|
+
The engine pins `controllers/activeadmin_batched_export/batched_export_controller` on both the host and ActiveAdmin importmaps. Include the gem asset path in your ActiveAdmin importmap cache sweeper when developing locally.
|
|
161
|
+
|
|
162
|
+
## How it works
|
|
163
|
+
|
|
164
|
+
1. Index download links route to `batched_export` instead of synchronous format URLs.
|
|
165
|
+
2. Workspace shows filter context, optional column checkboxes, and batch metadata.
|
|
166
|
+
3. Stimulus fetches `export_meta` JSON, then each `batch_page` chunk.
|
|
167
|
+
4. User saves the assembled Blob locally.
|
|
168
|
+
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
Each batch page uses offset pagination on the filtered collection. Later batches can slow down on very large tables; narrowing filters or raising `batch_size` within `max_batch_size` reduces batch count.
|
|
172
|
+
|
|
173
|
+
JSON and XML exports use the same column definitions as `csv` blocks. JSON batches return arrays of row objects; the client merges them 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.
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
From the gem root:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
bundle install
|
|
183
|
+
bundle exec appraisal install
|
|
184
|
+
bundle exec rubocop
|
|
185
|
+
bundle exec polyrun parallel-rspec --workers 5 --merge-failures
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Matrixed Rails versions use [Appraisal](https://github.com/thoughtbot/appraisal): `gemfiles/rails72.gemfile`, `rails8ruby34.gemfile`, and `rails8truffleruby.gemfile`. Run `bundle exec appraisal rspec` to execute RSpec in each gemfile context.
|
|
189
|
+
|
|
190
|
+
Shared agent guidance is managed with [pray](https://github.com/kiskolabs/pray) via `Prayfile`.
|
|
191
|
+
|
|
192
|
+
[Trunk](https://docs.trunk.io) config lives in `.trunk/`; CI can run `trunk` via `.github/workflows/_trunk_check.yml`. Releases: `make release` or `usr/bin/release.rb`.
|
|
193
|
+
|
|
194
|
+
## Contributing
|
|
195
|
+
|
|
196
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports and pull requests are welcome at https://github.com/amkisko/activeadmin_batched_export.rb/issues
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT — see [LICENSE.md](LICENSE.md).
|
|
201
|
+
|
|
202
|
+
## Sponsors
|
|
203
|
+
|
|
204
|
+
Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
205
|
+
|
|
206
|
+
<a href="https://www.kiskolabs.com">
|
|
207
|
+
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
208
|
+
</a>
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SECURITY
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Do NOT open a public GitHub issue for security vulnerabilities.
|
|
6
|
+
|
|
7
|
+
Email security details to: security@kiskolabs.com
|
|
8
|
+
|
|
9
|
+
Include: description, steps to reproduce, potential impact, and suggested fix (if available).
|
|
10
|
+
|
|
11
|
+
### Response Timeline
|
|
12
|
+
|
|
13
|
+
- We will acknowledge receipt of your report
|
|
14
|
+
- We will provide an initial assessment
|
|
15
|
+
- We will keep you informed of our progress and resolution timeline
|
|
16
|
+
|
|
17
|
+
### Disclosure Policy
|
|
18
|
+
|
|
19
|
+
- We will work with you to understand and resolve the issue
|
|
20
|
+
- We will credit you for the discovery (unless you prefer to remain anonymous)
|
|
21
|
+
- We will publish a security advisory after the vulnerability is patched
|
|
22
|
+
- We will coordinate public disclosure with you
|
|
23
|
+
|
|
24
|
+
## Automation Security
|
|
25
|
+
|
|
26
|
+
* Context Isolation: It is strictly forbidden to include production credentials, API keys, or Personally Identifiable Information (PII) in prompts sent to third-party LLMs or automation services.
|
|
27
|
+
|
|
28
|
+
* Supply Chain: All automated dependencies must be verified.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/activeadmin/batched_export/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "activeadmin_batched_export"
|
|
7
|
+
spec.version = ActiveAdmin::BatchedExport::VERSION
|
|
8
|
+
spec.authors = ["Andrei Makarov"]
|
|
9
|
+
spec.email = ["contact@kiskolabs.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Batched CSV, JSON, and XML export workspace for ActiveAdmin 4"
|
|
12
|
+
spec.description = "Replaces long-lived ActiveAdmin index downloads with a batched export workspace, customizable views, and export column macros."
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.platform = Gem::Platform::RUBY
|
|
15
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
16
|
+
|
|
17
|
+
repository_url = "https://github.com/amkisko/activeadmin_batched_export.rb"
|
|
18
|
+
|
|
19
|
+
spec.homepage = repository_url
|
|
20
|
+
spec.metadata = {
|
|
21
|
+
"homepage_uri" => repository_url,
|
|
22
|
+
"source_code_uri" => "#{repository_url}/tree/main",
|
|
23
|
+
"changelog_uri" => "#{repository_url}/blob/main/CHANGELOG.md",
|
|
24
|
+
"bug_tracker_uri" => "#{repository_url}/issues",
|
|
25
|
+
"documentation_uri" => "#{repository_url}#readme",
|
|
26
|
+
"rubygems_mfa_required" => "true"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spec.files = Dir.chdir(__dir__) do
|
|
30
|
+
%w[
|
|
31
|
+
CHANGELOG.md
|
|
32
|
+
CODE_OF_CONDUCT.md
|
|
33
|
+
CONTRIBUTING.md
|
|
34
|
+
GOVERNANCE.md
|
|
35
|
+
LICENSE.md
|
|
36
|
+
README.md
|
|
37
|
+
SECURITY.md
|
|
38
|
+
activeadmin_batched_export.gemspec
|
|
39
|
+
].select { |path| File.file?(path) } +
|
|
40
|
+
Dir["{app,config,lib,sig}/**/*"]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
spec.require_paths = ["lib"]
|
|
44
|
+
|
|
45
|
+
spec.add_dependency "activeadmin", ">= 4.0.0.beta13", "< 5"
|
|
46
|
+
spec.add_dependency "importmap-rails"
|
|
47
|
+
spec.add_dependency "rails", ">= 7.1"
|
|
48
|
+
|
|
49
|
+
spec.add_development_dependency "appraisal", "~> 2"
|
|
50
|
+
spec.add_development_dependency "bundler", ">= 2"
|
|
51
|
+
spec.add_development_dependency "polyrun", ">= 2.2.0"
|
|
52
|
+
spec.add_development_dependency "prosopite", "~> 2.0"
|
|
53
|
+
spec.add_development_dependency "rake", "~> 13"
|
|
54
|
+
spec.add_development_dependency "rspec", "~> 3"
|
|
55
|
+
spec.add_development_dependency "rspec-rails", ">= 6"
|
|
56
|
+
spec.add_development_dependency "propshaft", ">= 1.0"
|
|
57
|
+
spec.add_development_dependency "sqlite3", ">= 1"
|
|
58
|
+
spec.add_development_dependency "rubocop-rails", "~> 2.34"
|
|
59
|
+
spec.add_development_dependency "rubocop-rspec", "~> 3.8"
|
|
60
|
+
spec.add_development_dependency "rubocop-thread_safety", "~> 0.7"
|
|
61
|
+
spec.add_development_dependency "standard", "~> 1.52"
|
|
62
|
+
spec.add_development_dependency "standard-custom", "~> 1.0"
|
|
63
|
+
spec.add_development_dependency "standard-performance", "~> 1.8"
|
|
64
|
+
spec.add_development_dependency "standard-rails", "~> 1.5"
|
|
65
|
+
spec.add_development_dependency "standard-rspec", "~> 0.3"
|
|
66
|
+
spec.add_development_dependency "rbs", "~> 3"
|
|
67
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Progressive ActiveAdmin export: sequential batch fetches + single client-side save.
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ["progressWrap", "status", "fraction", "bar", "error", "start", "save", "columnCheckbox"]
|
|
6
|
+
static values = {
|
|
7
|
+
metaUrl: String,
|
|
8
|
+
meta: Object,
|
|
9
|
+
batchBaseUrl: String,
|
|
10
|
+
format: String,
|
|
11
|
+
preparingMessage: String,
|
|
12
|
+
loadingBatchTemplate: String,
|
|
13
|
+
emptyMessage: String,
|
|
14
|
+
failedBatchTemplate: String,
|
|
15
|
+
readyMessage: String,
|
|
16
|
+
needsColumnMessage: String,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
connect() {
|
|
20
|
+
this.parts = []
|
|
21
|
+
this.filename = null
|
|
22
|
+
this.mime = "application/octet-stream"
|
|
23
|
+
this.readyBlob = null
|
|
24
|
+
this.hideError()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
hideError() {
|
|
28
|
+
if (!this.hasErrorTarget) return
|
|
29
|
+
this.errorTarget.classList.add("hidden")
|
|
30
|
+
this.errorTarget.textContent = ""
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
showError(message) {
|
|
34
|
+
if (!this.hasErrorTarget) return
|
|
35
|
+
this.errorTarget.textContent = message
|
|
36
|
+
this.errorTarget.classList.remove("hidden")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
loadingLabel(current, total) {
|
|
40
|
+
return this.loadingBatchTemplateValue
|
|
41
|
+
.replace("%{current}", String(current))
|
|
42
|
+
.replace("%{total}", String(total))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Appends export_columns[]=… from checked column checkboxes (all checked by default). */
|
|
46
|
+
urlWithExportColumns(urlString) {
|
|
47
|
+
const u = new URL(urlString, window.location.origin)
|
|
48
|
+
u.searchParams.delete("export_columns[]")
|
|
49
|
+
u.searchParams.delete("export_columns")
|
|
50
|
+
|
|
51
|
+
if (!this.hasColumnCheckboxTarget) return u.toString()
|
|
52
|
+
|
|
53
|
+
const checked = this.columnCheckboxTargets.filter((cb) => cb.checked)
|
|
54
|
+
if (checked.length === 0) {
|
|
55
|
+
throw new Error(this.needsColumnMessageValue)
|
|
56
|
+
}
|
|
57
|
+
checked.forEach((cb) => u.searchParams.append("export_columns[]", cb.value))
|
|
58
|
+
return u.toString()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async start() {
|
|
62
|
+
this.hideError()
|
|
63
|
+
this.parts = []
|
|
64
|
+
this.readyBlob = null
|
|
65
|
+
this.filename = null
|
|
66
|
+
|
|
67
|
+
if (this.hasStartTarget) this.startTarget.disabled = true
|
|
68
|
+
if (this.hasSaveTarget) {
|
|
69
|
+
this.saveTarget.disabled = true
|
|
70
|
+
this.saveTarget.classList.add("hidden")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.hasProgressWrapTarget) this.progressWrapTarget.classList.remove("hidden")
|
|
74
|
+
if (this.hasBarTarget) {
|
|
75
|
+
this.barTarget.value = 0
|
|
76
|
+
this.barTarget.max = 100
|
|
77
|
+
}
|
|
78
|
+
if (this.hasStatusTarget) this.statusTarget.textContent = this.preparingMessageValue
|
|
79
|
+
if (this.hasFractionTarget) this.fractionTarget.textContent = ""
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const batchBaseUrl = this.urlWithExportColumns(this.batchBaseUrlValue)
|
|
83
|
+
|
|
84
|
+
let meta = this.hasMetaValue ? this.metaValue : null
|
|
85
|
+
if (!meta || typeof meta.total_batches !== "number") {
|
|
86
|
+
const metaUrl = this.urlWithExportColumns(this.metaUrlValue)
|
|
87
|
+
const metaRes = await fetch(metaUrl, {
|
|
88
|
+
credentials: "same-origin",
|
|
89
|
+
headers: { Accept: "application/json", "X-Requested-With": "XMLHttpRequest" },
|
|
90
|
+
})
|
|
91
|
+
if (!metaRes.ok) throw new Error(`${metaRes.status} ${metaRes.statusText}`)
|
|
92
|
+
meta = await metaRes.json()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.filename = meta.filename
|
|
96
|
+
const total = meta.total_batches
|
|
97
|
+
const exportFmt = meta.export_format || this.formatValue
|
|
98
|
+
|
|
99
|
+
if (total === 0) {
|
|
100
|
+
if (this.hasStatusTarget) this.statusTarget.textContent = this.emptyMessageValue
|
|
101
|
+
if (this.hasBarTarget) this.barTarget.removeAttribute("value")
|
|
102
|
+
if (this.hasStartTarget) this.startTarget.disabled = false
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (exportFmt === "csv") {
|
|
107
|
+
this.mime = "text/csv;charset=utf-8"
|
|
108
|
+
} else if (exportFmt === "json") {
|
|
109
|
+
this.mime = "application/json;charset=utf-8"
|
|
110
|
+
} else if (exportFmt === "xml") {
|
|
111
|
+
this.mime = "application/xml;charset=utf-8"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const collectedJsonRows = []
|
|
115
|
+
const xmlFragments = []
|
|
116
|
+
|
|
117
|
+
for (let page = 1; page <= total; page += 1) {
|
|
118
|
+
if (this.hasStatusTarget) this.statusTarget.textContent = this.loadingLabel(page, total)
|
|
119
|
+
if (this.hasFractionTarget) this.fractionTarget.textContent = `${page} / ${total}`
|
|
120
|
+
|
|
121
|
+
const batchUrl = new URL(batchBaseUrl, window.location.origin)
|
|
122
|
+
batchUrl.searchParams.set("batch_page", String(page))
|
|
123
|
+
|
|
124
|
+
const batchRes = await fetch(batchUrl.toString(), {
|
|
125
|
+
credentials: "same-origin",
|
|
126
|
+
headers: { Accept: "*/*", "X-Requested-With": "XMLHttpRequest" },
|
|
127
|
+
})
|
|
128
|
+
if (!batchRes.ok) {
|
|
129
|
+
const msg = this.failedBatchTemplateValue
|
|
130
|
+
.replace("%{page}", String(page))
|
|
131
|
+
.replace("%{message}", `${batchRes.status} ${batchRes.statusText}`)
|
|
132
|
+
throw new Error(msg)
|
|
133
|
+
}
|
|
134
|
+
const text = await batchRes.text()
|
|
135
|
+
|
|
136
|
+
if (exportFmt === "csv") {
|
|
137
|
+
this.parts.push(text)
|
|
138
|
+
} else if (exportFmt === "json") {
|
|
139
|
+
let chunk
|
|
140
|
+
try {
|
|
141
|
+
chunk = JSON.parse(text)
|
|
142
|
+
} catch (parseErr) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
this.failedBatchTemplateValue
|
|
145
|
+
.replace("%{page}", String(page))
|
|
146
|
+
.replace("%{message}", parseErr.message || "invalid JSON"),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
collectedJsonRows.push(...chunk)
|
|
150
|
+
} else if (exportFmt === "xml") {
|
|
151
|
+
xmlFragments.push(text.trim())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (this.hasBarTarget) this.barTarget.value = Math.round((100 * page) / total)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (exportFmt === "json") {
|
|
158
|
+
this.parts = [JSON.stringify(collectedJsonRows)]
|
|
159
|
+
} else if (exportFmt === "xml") {
|
|
160
|
+
this.parts = [
|
|
161
|
+
`<?xml version="1.0" encoding="UTF-8"?>\n<export>\n${xmlFragments.join("\n")}\n</export>\n`,
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.readyBlob = new Blob(this.parts, { type: this.mime })
|
|
166
|
+
if (this.hasSaveTarget) {
|
|
167
|
+
this.saveTarget.classList.remove("hidden")
|
|
168
|
+
this.saveTarget.disabled = false
|
|
169
|
+
}
|
|
170
|
+
if (this.hasStatusTarget) this.statusTarget.textContent = this.readyMessageValue
|
|
171
|
+
} catch (err) {
|
|
172
|
+
this.showError(err.message || String(err))
|
|
173
|
+
} finally {
|
|
174
|
+
if (this.hasStartTarget) this.startTarget.disabled = false
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
save() {
|
|
179
|
+
if (!this.readyBlob || !this.filename) return
|
|
180
|
+
const url = URL.createObjectURL(this.readyBlob)
|
|
181
|
+
const anchor = document.createElement("a")
|
|
182
|
+
anchor.href = url
|
|
183
|
+
anchor.download = this.filename
|
|
184
|
+
anchor.rel = "noopener"
|
|
185
|
+
anchor.click()
|
|
186
|
+
URL.revokeObjectURL(url)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<div class="<%= styles[:actions] %>">
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
class="<%= styles[:primary_button] %>"
|
|
5
|
+
data-action="<%= stimulus_controller %>#start"
|
|
6
|
+
data-<%= stimulus_controller %>-target="start"
|
|
7
|
+
>
|
|
8
|
+
<%= t("active_admin.batched_export_page.start") %>
|
|
9
|
+
</button>
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
class="<%= styles[:secondary_button] %>"
|
|
13
|
+
data-<%= stimulus_controller %>-target="save"
|
|
14
|
+
data-action="<%= stimulus_controller %>#save"
|
|
15
|
+
disabled
|
|
16
|
+
>
|
|
17
|
+
<%= t("active_admin.batched_export_page.save_file") %>
|
|
18
|
+
</button>
|
|
19
|
+
<%= link_to t("active_admin.batched_export_page.back_to_list"),
|
|
20
|
+
url_for(
|
|
21
|
+
action: :index,
|
|
22
|
+
params: request.query_parameters.except(:format, :commit, :page, :batch_page, :export_meta)
|
|
23
|
+
),
|
|
24
|
+
class: styles[:back_link] %>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<% if @batched_export_csv_columns.present? %>
|
|
2
|
+
<div class="<%= styles[:card] %>">
|
|
3
|
+
<h3 class="<%= styles[:card_title] %>"><%= t("active_admin.batched_export_page.columns_title") %></h3>
|
|
4
|
+
<p class="<%= styles[:hint] %>"><%= t("active_admin.batched_export_page.columns_hint") %></p>
|
|
5
|
+
<div class="<%= styles[:column_grid] %>">
|
|
6
|
+
<% @batched_export_csv_columns.each do |column| %>
|
|
7
|
+
<label class="<%= styles[:column_label] %>">
|
|
8
|
+
<input
|
|
9
|
+
type="checkbox"
|
|
10
|
+
name="export_columns[]"
|
|
11
|
+
value="<%= column[:index] %>"
|
|
12
|
+
checked
|
|
13
|
+
class="<%= styles[:column_checkbox] %>"
|
|
14
|
+
data-<%= stimulus_controller %>-target="columnCheckbox"
|
|
15
|
+
/>
|
|
16
|
+
<span><%= column[:label] %></span>
|
|
17
|
+
</label>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|