admin_suite 0.2.0 → 0.2.1
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 +4 -4
- data/app/controllers/admin_suite/application_controller.rb +1 -2
- data/docs/configuration.md +30 -5
- data/lib/admin/base/action_executor.rb +69 -0
- data/lib/admin_suite/configuration.rb +2 -0
- data/lib/admin_suite/engine.rb +73 -31
- data/lib/admin_suite/version.rb +1 -1
- data/lib/generators/admin_suite/install/templates/admin_suite.rb +8 -0
- data/test/dummy/log/test.log +624 -0
- data/test/dummy/tmp/local_secret.txt +1 -0
- data/test/lib/action_executor_test.rb +172 -0
- data/test/lib/zeitwerk_integration_test.rb +69 -16
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a3f83815ccce6bcb0e0a38e07e0b90d6d969309b2e3302e1870135f02793762
|
|
4
|
+
data.tar.gz: 2564e620eaa8c66030d8f774b1d4246eaa4359955ce2a5670f62f069a88a58af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '071608f05dc059b788b90350e9cfbd590d417bf124f3327dbf151202d32f2c71359f3c6ba442e13fba7138d8fc7b28d96e52687825bf64beb66c4058dd640eb9'
|
|
7
|
+
data.tar.gz: ac7a393a423342b228c9489492d361e34f98afaaf9e9527a1a71639ecc3d0ee7ce59ddfae3988d1d83ccac474a69764ce7b7d7afed0e346026642bd9c69bb3f2
|
|
@@ -35,12 +35,11 @@ module AdminSuite
|
|
|
35
35
|
nil
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
# Loads resource definition files in
|
|
38
|
+
# Loads resource definition files when needed (runs in all environments).
|
|
39
39
|
#
|
|
40
40
|
# @return [void]
|
|
41
41
|
def ensure_resources_loaded!
|
|
42
42
|
require "admin/base/resource" unless defined?(Admin::Base::Resource)
|
|
43
|
-
return unless Rails.env.development?
|
|
44
43
|
return if Admin::Base::Resource.registered_resources.any?
|
|
45
44
|
|
|
46
45
|
Array(AdminSuite.config.resource_globs).flat_map { |g| Dir[g] }.uniq.each do |file|
|
data/docs/configuration.md
CHANGED
|
@@ -32,16 +32,27 @@ These are the defaults in `AdminSuite::Configuration` / `AdminSuite::Engine`:
|
|
|
32
32
|
- `resource_globs`: defaults to:
|
|
33
33
|
- `Rails.root/config/admin_suite/resources/*.rb`
|
|
34
34
|
- `Rails.root/app/admin/resources/*.rb`
|
|
35
|
+
- `action_globs`: defaults to:
|
|
36
|
+
- `Rails.root/config/admin_suite/actions/*.rb`
|
|
37
|
+
- `Rails.root/app/admin/actions/*.rb`
|
|
35
38
|
- `portal_globs`: defaults to:
|
|
36
39
|
- `Rails.root/config/admin_suite/portals/*.rb`
|
|
37
40
|
- `Rails.root/app/admin/portals/*.rb`
|
|
38
41
|
- `Rails.root/app/admin_suite/portals/*.rb`
|
|
39
42
|
|
|
40
|
-
Note:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`app/
|
|
43
|
+
Note: AdminSuite definition files (resources, actions, portals) often don't follow
|
|
44
|
+
Zeitwerk's path-to-constant naming conventions. To prevent eager-load `Zeitwerk::NameError`s
|
|
45
|
+
in production, the engine only configures Zeitwerk to ignore these directories and load them via globs instead:
|
|
46
|
+
- `app/admin_suite`
|
|
47
|
+
- `app/admin/portals` (when portal DSL usage is detected)
|
|
48
|
+
|
|
49
|
+
Other `app/admin/*` directories (such as `app/admin/resources`, `app/admin/actions`, and `app/admin/base`) are
|
|
50
|
+
not ignored by default and may be treated as normal Zeitwerk autoload paths if they are added to the loader
|
|
51
|
+
(for example, via `loader.push_dir("app/admin")` in the host app). Do not rely on these directories being
|
|
52
|
+
ignored for autoloading; instead, keep files there Zeitwerk-compatible.
|
|
53
|
+
|
|
54
|
+
We recommend placing non-Zeitwerk-compatible definition files under `config/admin_suite/*` or `app/admin_suite/*`
|
|
55
|
+
for clearer separation from standard Rails autoloading.
|
|
45
56
|
- `portals`: default portal metadata for `:ops`, `:email`, `:ai`, `:assistant`
|
|
46
57
|
- `custom_renderers`: `{}`
|
|
47
58
|
- `icon_renderer`: `nil` (uses lucide-rails by default when available)
|
|
@@ -95,6 +106,20 @@ config.resource_globs = [
|
|
|
95
106
|
]
|
|
96
107
|
```
|
|
97
108
|
|
|
109
|
+
### `action_globs`
|
|
110
|
+
|
|
111
|
+
Where AdminSuite should load action handler files from (files that define custom action handlers, typically subclasses of `Admin::Base::ActionHandler`).
|
|
112
|
+
|
|
113
|
+
- **Type**: `Array<String>`
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
config.action_globs = [
|
|
119
|
+
Rails.root.join("app/admin/actions/**/*.rb").to_s
|
|
120
|
+
]
|
|
121
|
+
```
|
|
122
|
+
|
|
98
123
|
### `portal_globs`
|
|
99
124
|
|
|
100
125
|
Where AdminSuite should load portal definition files from (files typically call `AdminSuite.portal(:key) { ... }`).
|
|
@@ -12,6 +12,13 @@ module Admin
|
|
|
12
12
|
def failure? = !success
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# Track whether action handlers have been loaded to avoid repeated expensive globs
|
|
16
|
+
@handlers_loaded = false
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
attr_accessor :handlers_loaded
|
|
20
|
+
end
|
|
21
|
+
|
|
15
22
|
def initialize(resource_class, action_name, actor)
|
|
16
23
|
@resource_class = resource_class
|
|
17
24
|
@action_name = action_name
|
|
@@ -116,6 +123,17 @@ module Admin
|
|
|
116
123
|
return resolved if resolved
|
|
117
124
|
end
|
|
118
125
|
|
|
126
|
+
handler_name = "#{resource_class.resource_name.camelize}#{action.name.to_s.camelize}Action"
|
|
127
|
+
handler_constant = "Admin::Actions::#{handler_name}"
|
|
128
|
+
handler_constant.constantize
|
|
129
|
+
rescue NameError
|
|
130
|
+
# In many host apps, action handlers live under `app/admin/actions/**`.
|
|
131
|
+
# Rails treats `app/admin` as a Zeitwerk root, which means Zeitwerk expects
|
|
132
|
+
# top-level constants (e.g. `Actions::Foo`) unless the host configures
|
|
133
|
+
# a namespace mapping. AdminSuite avoids requiring host Zeitwerk setup by
|
|
134
|
+
# loading handler files via `AdminSuite.config.action_globs` when needed.
|
|
135
|
+
load_action_handlers_for_admin_suite!
|
|
136
|
+
|
|
119
137
|
handler_name = "#{resource_class.resource_name.camelize}#{action.name.to_s.camelize}Action"
|
|
120
138
|
"Admin::Actions::#{handler_name}".constantize
|
|
121
139
|
rescue NameError
|
|
@@ -150,6 +168,57 @@ module Admin
|
|
|
150
168
|
rescue StandardError
|
|
151
169
|
nil
|
|
152
170
|
end
|
|
171
|
+
|
|
172
|
+
def load_action_handlers_for_admin_suite!
|
|
173
|
+
return unless defined?(AdminSuite)
|
|
174
|
+
|
|
175
|
+
# Track whether we've already loaded handlers to avoid expensive repeated globs.
|
|
176
|
+
# In development, this flag is reset by the Rails reloader (see engine.rb).
|
|
177
|
+
# In production/test, it persists for the process lifetime.
|
|
178
|
+
return if self.class.handlers_loaded
|
|
179
|
+
|
|
180
|
+
files = Array(AdminSuite.config.action_globs).flat_map { |g| Dir[g] }.uniq
|
|
181
|
+
|
|
182
|
+
# Set the flag even if no files found - we've done the glob and shouldn't repeat it
|
|
183
|
+
if files.empty?
|
|
184
|
+
self.class.handlers_loaded = true
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
files.each do |file|
|
|
189
|
+
begin
|
|
190
|
+
if Rails.env.development?
|
|
191
|
+
load file
|
|
192
|
+
else
|
|
193
|
+
require file
|
|
194
|
+
end
|
|
195
|
+
rescue StandardError, ScriptError => e
|
|
196
|
+
log_action_handler_load_error(file, e)
|
|
197
|
+
|
|
198
|
+
# Fail fast in dev/test so broken handler files are immediately discoverable.
|
|
199
|
+
raise if Rails.env.development? || Rails.env.test?
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# We attempted to load the configured handlers. Avoid repeating expensive globs
|
|
204
|
+
# and file loads for the rest of the process lifetime.
|
|
205
|
+
self.class.handlers_loaded = true
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def log_action_handler_load_error(file, error)
|
|
209
|
+
message = "[AdminSuite] Failed to load action handler file #{file}: #{error.class}: #{error.message}"
|
|
210
|
+
|
|
211
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
212
|
+
Rails.logger.error(message)
|
|
213
|
+
|
|
214
|
+
backtrace = Array(error.backtrace).take(20).join("\n")
|
|
215
|
+
Rails.logger.error(backtrace) unless backtrace.empty?
|
|
216
|
+
else
|
|
217
|
+
warn(message)
|
|
218
|
+
end
|
|
219
|
+
rescue StandardError
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
153
222
|
end
|
|
154
223
|
end
|
|
155
224
|
end
|
|
@@ -7,6 +7,7 @@ module AdminSuite
|
|
|
7
7
|
:current_actor,
|
|
8
8
|
:authorize,
|
|
9
9
|
:resource_globs,
|
|
10
|
+
:action_globs,
|
|
10
11
|
:portal_globs,
|
|
11
12
|
:portals,
|
|
12
13
|
:custom_renderers,
|
|
@@ -25,6 +26,7 @@ module AdminSuite
|
|
|
25
26
|
@current_actor = nil
|
|
26
27
|
@authorize = nil
|
|
27
28
|
@resource_globs = []
|
|
29
|
+
@action_globs = []
|
|
28
30
|
@portal_globs = []
|
|
29
31
|
@portals = {}
|
|
30
32
|
@custom_renderers = {}
|
data/lib/admin_suite/engine.rb
CHANGED
|
@@ -13,41 +13,65 @@ module AdminSuite
|
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
initializer "admin_suite.host_dsl_ignore" do
|
|
17
|
-
# Host apps
|
|
18
|
-
#
|
|
16
|
+
initializer "admin_suite.host_dsl_ignore", before: :setup_main_autoloader do |app|
|
|
17
|
+
# Host apps may store AdminSuite DSL files under `app/admin_suite/**` and
|
|
18
|
+
# `app/admin/portals/**`.
|
|
19
19
|
#
|
|
20
|
-
# These are
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# `app/admin
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
rescue StandardError
|
|
41
|
-
false
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
host_dsl_dirs << host_admin_portals_dir if contains_admin_suite_portals
|
|
20
|
+
# These are side-effect DSL files (they do not define constants), so Zeitwerk
|
|
21
|
+
# must ignore them to avoid eager-load `Zeitwerk::NameError`s in production.
|
|
22
|
+
|
|
23
|
+
admin_suite_app_dir = Rails.root.join("app/admin_suite")
|
|
24
|
+
admin_dir = Rails.root.join("app/admin")
|
|
25
|
+
admin_portals_dir = Rails.root.join("app/admin/portals")
|
|
26
|
+
|
|
27
|
+
# If the host uses `Admin::*` constants inside `app/admin/**`, Rails' default
|
|
28
|
+
# autoload root (`app/admin`) would expect top-level constants like
|
|
29
|
+
# `Resources::UserResource`. We fix that by mapping `app/admin` to `Admin`.
|
|
30
|
+
# This avoids requiring host apps to add their own Zeitwerk initializer.
|
|
31
|
+
if admin_dir.exist? && self.class.host_admin_namespace_files?(admin_dir)
|
|
32
|
+
admin_dir_s = admin_dir.to_s
|
|
33
|
+
app.config.autoload_paths.delete(admin_dir_s)
|
|
34
|
+
app.config.eager_load_paths.delete(admin_dir_s)
|
|
35
|
+
|
|
36
|
+
# Ensure `Admin` exists so Zeitwerk can use it as a namespace.
|
|
37
|
+
module ::Admin; end
|
|
38
|
+
|
|
39
|
+
Rails.autoloaders.main.push_dir(admin_dir, namespace: ::Admin)
|
|
45
40
|
end
|
|
46
41
|
|
|
47
42
|
Rails.autoloaders.each do |loader|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
loader.ignore(admin_suite_app_dir) if admin_suite_app_dir.exist?
|
|
44
|
+
|
|
45
|
+
next unless admin_portals_dir.exist?
|
|
46
|
+
|
|
47
|
+
loader.ignore(admin_portals_dir) if self.class.contains_admin_suite_portal_dsl?(admin_portals_dir)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.host_admin_namespace_files?(admin_dir)
|
|
52
|
+
# True if any file under app/admin appears to define `Admin::*` constants.
|
|
53
|
+
Dir[admin_dir.join("**/*.rb").to_s].any? do |file|
|
|
54
|
+
next false if file.include?("/portals/")
|
|
55
|
+
|
|
56
|
+
content = File.binread(file)
|
|
57
|
+
content = content.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
58
|
+
|
|
59
|
+
content.match?(/\b(module|class)\s+Admin\b/) ||
|
|
60
|
+
content.match?(/\b(module|class)\s+Admin::/)
|
|
61
|
+
rescue StandardError
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.contains_admin_suite_portal_dsl?(admin_portals_dir)
|
|
67
|
+
portal_files = Dir[admin_portals_dir.join("**/*.rb").to_s]
|
|
68
|
+
portal_files.any? do |file|
|
|
69
|
+
content = File.binread(file)
|
|
70
|
+
content = content.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
71
|
+
portal_dsl_pattern = /(::)?AdminSuite\s*\.\s*portal\b/
|
|
72
|
+
portal_dsl_pattern.match?(content)
|
|
73
|
+
rescue StandardError
|
|
74
|
+
false
|
|
51
75
|
end
|
|
52
76
|
end
|
|
53
77
|
|
|
@@ -59,6 +83,17 @@ module AdminSuite
|
|
|
59
83
|
require "admin/base/action_handler"
|
|
60
84
|
end
|
|
61
85
|
|
|
86
|
+
initializer "admin_suite.reloader" do |app|
|
|
87
|
+
# Reset the handlers_loaded flag in development so handlers are reloaded
|
|
88
|
+
# when code changes. This ensures the expensive glob operation happens at
|
|
89
|
+
# most once per request (or code reload) rather than on every NameError.
|
|
90
|
+
if Rails.env.development?
|
|
91
|
+
app.reloader.to_prepare do
|
|
92
|
+
Admin::Base::ActionExecutor.handlers_loaded = false
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
62
97
|
initializer "admin_suite.watchable_dirs" do |app|
|
|
63
98
|
next unless Rails.env.development?
|
|
64
99
|
|
|
@@ -91,6 +126,13 @@ module AdminSuite
|
|
|
91
126
|
]
|
|
92
127
|
end
|
|
93
128
|
|
|
129
|
+
if config.action_globs.blank?
|
|
130
|
+
config.action_globs = [
|
|
131
|
+
Rails.root.join("config/admin_suite/actions/*.rb").to_s,
|
|
132
|
+
Rails.root.join("app/admin/actions/*.rb").to_s
|
|
133
|
+
]
|
|
134
|
+
end
|
|
135
|
+
|
|
94
136
|
if config.portal_globs.blank?
|
|
95
137
|
config.portal_globs = [
|
|
96
138
|
Rails.root.join("config/admin_suite/portals/*.rb").to_s,
|
data/lib/admin_suite/version.rb
CHANGED
|
@@ -20,6 +20,14 @@ AdminSuite.configure do |config|
|
|
|
20
20
|
Rails.root.join("app/admin/resources/*.rb").to_s
|
|
21
21
|
]
|
|
22
22
|
|
|
23
|
+
# Action handler file globs (host app can override).
|
|
24
|
+
#
|
|
25
|
+
# Files typically define `Admin::Actions::<Resource><Action>Action` handlers.
|
|
26
|
+
config.action_globs = [
|
|
27
|
+
Rails.root.join("config/admin_suite/actions/*.rb").to_s,
|
|
28
|
+
Rails.root.join("app/admin/actions/*.rb").to_s
|
|
29
|
+
]
|
|
30
|
+
|
|
23
31
|
# Portal dashboard DSL globs (host app can override).
|
|
24
32
|
# Files typically call `AdminSuite.portal :ops do ... end`
|
|
25
33
|
config.portal_globs = [
|