admin_suite 0.2.0 → 0.2.2

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/admin_suite.css +128 -0
  3. data/app/controllers/admin_suite/application_controller.rb +32 -2
  4. data/app/controllers/admin_suite/dashboard_controller.rb +59 -226
  5. data/app/helpers/admin_suite/base_helper.rb +108 -108
  6. data/app/helpers/admin_suite/panels_helper.rb +1 -1
  7. data/app/javascript/controllers/admin_suite/file_upload_controller.js +9 -9
  8. data/app/javascript/controllers/admin_suite/json_editor_controller.js +8 -8
  9. data/app/javascript/controllers/admin_suite/searchable_select_controller.js +2 -2
  10. data/app/javascript/controllers/admin_suite/tag_select_controller.js +1 -1
  11. data/app/javascript/controllers/admin_suite/toggle_switch_controller.js +1 -1
  12. data/app/views/admin_suite/dashboard/index.html.erb +6 -15
  13. data/app/views/admin_suite/panels/_cards.html.erb +6 -6
  14. data/app/views/admin_suite/panels/_chart.html.erb +12 -12
  15. data/app/views/admin_suite/panels/_health.html.erb +14 -14
  16. data/app/views/admin_suite/panels/_recent.html.erb +11 -11
  17. data/app/views/admin_suite/panels/_stat.html.erb +24 -24
  18. data/app/views/admin_suite/panels/_table.html.erb +10 -10
  19. data/app/views/admin_suite/portals/show.html.erb +1 -1
  20. data/app/views/admin_suite/resources/_form.html.erb +1 -1
  21. data/app/views/admin_suite/resources/edit.html.erb +4 -4
  22. data/app/views/admin_suite/resources/index.html.erb +23 -23
  23. data/app/views/admin_suite/resources/new.html.erb +4 -4
  24. data/app/views/admin_suite/resources/show.html.erb +17 -17
  25. data/app/views/admin_suite/shared/_form.html.erb +8 -8
  26. data/app/views/admin_suite/shared/_json_editor_field.html.erb +4 -4
  27. data/app/views/admin_suite/shared/_sidebar.html.erb +4 -4
  28. data/app/views/admin_suite/shared/_topbar.html.erb +1 -1
  29. data/app/views/layouts/admin_suite/application.html.erb +4 -4
  30. data/docs/configuration.md +56 -6
  31. data/docs/portals.md +42 -0
  32. data/lib/admin/base/action_executor.rb +69 -0
  33. data/lib/admin_suite/configuration.rb +12 -0
  34. data/lib/admin_suite/engine.rb +82 -31
  35. data/lib/admin_suite/ui/field_renderer_registry.rb +2 -2
  36. data/lib/admin_suite/ui/form_field_renderer.rb +2 -2
  37. data/lib/admin_suite/ui/show_formatter_registry.rb +5 -5
  38. data/lib/admin_suite/ui/show_value_formatter.rb +1 -1
  39. data/lib/admin_suite/version.rb +1 -1
  40. data/lib/admin_suite.rb +31 -0
  41. data/lib/generators/admin_suite/install/templates/admin_suite.rb +8 -0
  42. data/test/dummy/log/test.log +1512 -0
  43. data/test/dummy/tmp/local_secret.txt +1 -0
  44. data/test/integration/dashboard_test.rb +57 -1
  45. data/test/lib/action_executor_test.rb +172 -0
  46. data/test/lib/zeitwerk_integration_test.rb +69 -16
  47. metadata +4 -1
@@ -0,0 +1 @@
1
+ c02d71d37975d95a2c65ffeee97e37a5a969d38af1782fdc54d86db04188fdd28451c9f3a2be79410e2b66eede3b444ab30145534c96c013f3a6da03a3c32d1e
@@ -1,13 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_helper"
4
+ require "tmpdir"
4
5
 
5
6
  module AdminSuite
6
7
  class DashboardTest < ActionDispatch::IntegrationTest
7
8
  test "GET /internal/admin_suite renders dashboard" do
8
9
  get "/internal/admin_suite"
9
10
  assert_response :success
10
- assert_includes response.body, "Developer Portal"
11
+ assert_includes response.body, "Admin Suite"
12
+ end
13
+
14
+ test "loads custom root dashboard definition from dashboard_globs" do
15
+ old_globs = AdminSuite.config.dashboard_globs
16
+ old_title = AdminSuite.config.root_dashboard_title
17
+ old_description = AdminSuite.config.root_dashboard_description
18
+
19
+ Dir.mktmpdir("admin-suite-dashboard") do |dir|
20
+ dashboard_rb = File.join(dir, "dashboard.rb")
21
+
22
+ File.write(dashboard_rb, <<~'RUBY')
23
+ # frozen_string_literal: true
24
+
25
+ AdminSuite.configure do |config|
26
+ config.root_dashboard_title = ->(controller) { "Custom Root #{controller.request.path}" }
27
+ config.root_dashboard_description = ->(_controller) { "Custom root description" }
28
+ end
29
+
30
+ AdminSuite.root_dashboard do
31
+ row do
32
+ stat_panel "Custom A", -> { 11 }, span: 6, variant: :mini, color: :slate
33
+ stat_panel "Custom B", 22, span: 6, variant: :mini, color: :slate
34
+ end
35
+ end
36
+ RUBY
37
+
38
+ AdminSuite.reset_root_dashboard!
39
+ AdminSuite.config.dashboard_globs = [File.join(dir, "*.rb")]
40
+
41
+ get "/internal/admin_suite"
42
+ assert_response :success
43
+
44
+ # Title + description should come from the dashboard definition file.
45
+ assert_includes response.body, "Custom Root /internal/admin_suite"
46
+ assert_includes response.body, "Custom root description"
47
+
48
+ # DSL-driven panels should render.
49
+ assert_includes response.body, "Custom A"
50
+ assert_includes response.body, ">11<"
51
+ assert_includes response.body, "Custom B"
52
+ assert_includes response.body, ">22<"
53
+
54
+ # Ensure spans are emitted (used to drive column layout).
55
+ assert_includes response.body, "admin-suite-dashboard-row"
56
+ assert_equal 2, response.body.scan("grid-column: span 6 / span 6;").size
57
+
58
+ # Loader should mark the dashboard as loaded in non-dev envs (test).
59
+ assert AdminSuite.config.root_dashboard_loaded
60
+ assert AdminSuite.root_dashboard_definition.present?
61
+ end
62
+ ensure
63
+ AdminSuite.config.dashboard_globs = old_globs
64
+ AdminSuite.config.root_dashboard_title = old_title
65
+ AdminSuite.config.root_dashboard_description = old_description
66
+ AdminSuite.reset_root_dashboard!
11
67
  end
12
68
  end
13
69
  end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "tmpdir"
5
+
6
+ module Admin
7
+ module Base
8
+ class ActionExecutorTest < ActiveSupport::TestCase
9
+ setup do
10
+ @temp_dir = Dir.mktmpdir("admin_suite_test")
11
+ @original_config = AdminSuite.config.action_globs.dup
12
+
13
+ # Reset the handlers_loaded flag before each test
14
+ ActionExecutor.handlers_loaded = false
15
+ end
16
+
17
+ teardown do
18
+ FileUtils.rm_rf(@temp_dir) if @temp_dir && File.exist?(@temp_dir)
19
+ AdminSuite.config.action_globs = @original_config
20
+
21
+ # Reset the flag after each test
22
+ ActionExecutor.handlers_loaded = false
23
+ end
24
+
25
+ test "handlers_loaded flag starts as false" do
26
+ ActionExecutor.handlers_loaded = false
27
+ assert_equal false, ActionExecutor.handlers_loaded
28
+ end
29
+
30
+ test "handlers_loaded flag can be set to true" do
31
+ ActionExecutor.handlers_loaded = true
32
+ assert_equal true, ActionExecutor.handlers_loaded
33
+ end
34
+
35
+ test "load_action_handlers_for_admin_suite sets handlers_loaded to true after loading" do
36
+ # Create a temporary action handler file
37
+ actions_dir = File.join(@temp_dir, "actions")
38
+ FileUtils.mkdir_p(actions_dir)
39
+ File.write(
40
+ File.join(actions_dir, "test_action.rb"),
41
+ "module Admin\n module Actions\n class TestAction\n end\n end\nend"
42
+ )
43
+
44
+ # Configure AdminSuite to look in our temp directory
45
+ AdminSuite.config.action_globs = [ File.join(actions_dir, "*.rb") ]
46
+
47
+ # Create an executor instance and call the loading method
48
+ resource_class = Struct.new(:resource_name).new("test")
49
+ executor = ActionExecutor.new(resource_class, :test, nil)
50
+
51
+ # Ensure flag starts as false
52
+ ActionExecutor.handlers_loaded = false
53
+
54
+ # Call the private method
55
+ executor.send(:load_action_handlers_for_admin_suite!)
56
+
57
+ # Verify the flag is now true
58
+ assert ActionExecutor.handlers_loaded, "Expected handlers_loaded to be true after loading"
59
+ end
60
+
61
+ test "load_action_handlers_for_admin_suite skips loading when handlers_loaded is true" do
62
+ # Create a temporary action handler file
63
+ actions_dir = File.join(@temp_dir, "actions")
64
+ FileUtils.mkdir_p(actions_dir)
65
+ action_file = File.join(actions_dir, "test_action.rb")
66
+ File.write(action_file, "# Test action")
67
+
68
+ # Configure AdminSuite to look in our temp directory
69
+ AdminSuite.config.action_globs = [ File.join(actions_dir, "*.rb") ]
70
+
71
+ # Create an executor instance
72
+ resource_class = Struct.new(:resource_name).new("test")
73
+ executor = ActionExecutor.new(resource_class, :test, nil)
74
+
75
+ # Set the flag to true to simulate already loaded
76
+ ActionExecutor.handlers_loaded = true
77
+
78
+ # Mock Dir[] to verify it's not called
79
+ glob_called = false
80
+ original_bracket = Dir.method(:[])
81
+ Dir.define_singleton_method(:[]) do |pattern|
82
+ glob_called = true
83
+ original_bracket.call(pattern)
84
+ end
85
+
86
+ begin
87
+ # Call the loading method
88
+ executor.send(:load_action_handlers_for_admin_suite!)
89
+
90
+ # Verify glob was not called because handlers were already loaded
91
+ assert_not glob_called, "Expected Dir[] to not be called when handlers_loaded is true"
92
+ ensure
93
+ # Restore Dir.[] method
94
+ Dir.define_singleton_method(:[], original_bracket)
95
+ end
96
+ end
97
+
98
+ test "load_action_handlers_for_admin_suite returns early when AdminSuite is not defined" do
99
+ # Temporarily undefine AdminSuite to test early return
100
+ admin_suite_defined = defined?(AdminSuite)
101
+
102
+ skip "Cannot test AdminSuite undefined condition when AdminSuite is required" if admin_suite_defined
103
+
104
+ resource_class = Struct.new(:resource_name).new("test")
105
+ executor = ActionExecutor.new(resource_class, :test, nil)
106
+
107
+ # This should return early without error
108
+ assert_nil executor.send(:load_action_handlers_for_admin_suite!)
109
+ end
110
+
111
+ test "load_action_handlers_for_admin_suite handles empty action_globs gracefully" do
112
+ # Set action_globs to empty
113
+ AdminSuite.config.action_globs = []
114
+
115
+ resource_class = Struct.new(:resource_name).new("test")
116
+ executor = ActionExecutor.new(resource_class, :test, nil)
117
+
118
+ ActionExecutor.handlers_loaded = false
119
+
120
+ # This should not raise an error
121
+ assert_nothing_raised do
122
+ executor.send(:load_action_handlers_for_admin_suite!)
123
+ end
124
+
125
+ # Flag should be set even when no files exist to avoid repeated expensive globs
126
+ assert ActionExecutor.handlers_loaded, "Expected handlers_loaded to be true even when no files exist"
127
+ end
128
+
129
+ test "load_action_handlers_for_admin_suite handles errors gracefully" do
130
+ # Configure a glob pattern that will cause an error
131
+ AdminSuite.config.action_globs = [ "/nonexistent/path/*.rb" ]
132
+
133
+ resource_class = Struct.new(:resource_name).new("test")
134
+ executor = ActionExecutor.new(resource_class, :test, nil)
135
+
136
+ ActionExecutor.handlers_loaded = false
137
+
138
+ # This should not raise an error even if globbing fails
139
+ assert_nothing_raised do
140
+ executor.send(:load_action_handlers_for_admin_suite!)
141
+ end
142
+ end
143
+
144
+ test "load_action_handlers_for_admin_suite does not set flag when file loading fails" do
145
+ # Create a temporary action handler file with invalid Ruby
146
+ actions_dir = File.join(@temp_dir, "actions")
147
+ FileUtils.mkdir_p(actions_dir)
148
+ File.write(
149
+ File.join(actions_dir, "bad_action.rb"),
150
+ "this is not valid ruby syntax @#$%"
151
+ )
152
+
153
+ # Configure AdminSuite to look in our temp directory
154
+ AdminSuite.config.action_globs = [ File.join(actions_dir, "*.rb") ]
155
+
156
+ resource_class = Struct.new(:resource_name).new("test")
157
+ executor = ActionExecutor.new(resource_class, :test, nil)
158
+
159
+ ActionExecutor.handlers_loaded = false
160
+
161
+ # In test, we fail fast so syntax errors in handler files are discoverable.
162
+ assert_raises(SyntaxError) do
163
+ executor.send(:load_action_handlers_for_admin_suite!)
164
+ end
165
+
166
+ # Flag should remain false when loading fails, allowing retry
167
+ assert_not ActionExecutor.handlers_loaded,
168
+ "Expected handlers_loaded to remain false when file loading fails"
169
+ end
170
+ end
171
+ end
172
+ end
@@ -17,34 +17,55 @@ module AdminSuite
17
17
  def create_tracked_loader
18
18
  loader = Zeitwerk::Loader.new
19
19
  ignored_dirs = []
20
+ pushed_dirs = []
20
21
 
21
22
  loader.define_singleton_method(:ignore) do |path|
22
23
  ignored_dirs << path.to_s
23
24
  end
24
25
 
25
- [loader, ignored_dirs]
26
+ loader.define_singleton_method(:push_dir) do |path, namespace:|
27
+ pushed_dirs << { path: path.to_s, namespace: namespace }
28
+ end
29
+
30
+ [loader, ignored_dirs, pushed_dirs]
26
31
  end
27
32
 
28
33
  # Helper method that simulates the Zeitwerk integration logic from engine.rb
29
34
  def simulate_zeitwerk_integration(app_root, loader)
30
- host_dsl_dirs = [app_root.join("app/admin_suite")]
31
- host_admin_portals_dir = app_root.join("app/admin/portals")
35
+ admin_suite_app_dir = app_root.join("app/admin_suite")
36
+ admin_dir = app_root.join("app/admin")
37
+ admin_portals_dir = app_root.join("app/admin/portals")
32
38
 
33
- if host_admin_portals_dir.exist?
34
- portal_files = Dir[host_admin_portals_dir.join("**/*.rb").to_s]
35
- contains_admin_suite_portals =
36
- portal_files.any? do |file|
39
+ # Map app/admin -> Admin namespace if files define Admin::* constants.
40
+ if admin_dir.exist?
41
+ rb_files = Dir[admin_dir.join("**/*.rb").to_s]
42
+ rb_files.reject! { |f| f.include?("/portals/") }
43
+
44
+ host_uses_admin_namespace =
45
+ rb_files.any? do |file|
37
46
  content = File.binread(file).encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
38
- content.include?("AdminSuite.portal")
47
+ content.match?(/\b(module|class)\s+Admin\b/) || content.match?(/\b(module|class)\s+Admin::/)
39
48
  rescue StandardError
40
49
  false
41
50
  end
42
51
 
43
- host_dsl_dirs << host_admin_portals_dir if contains_admin_suite_portals
52
+ loader.push_dir(admin_dir, namespace: Admin) if host_uses_admin_namespace
44
53
  end
45
54
 
46
- host_dsl_dirs.each do |dir|
47
- loader.ignore(dir) if dir.exist?
55
+ loader.ignore(admin_suite_app_dir) if admin_suite_app_dir.exist?
56
+
57
+ # Ignore portal DSL files (side-effect DSL, not constants).
58
+ if admin_portals_dir.exist?
59
+ portal_files = Dir[admin_portals_dir.join("**/*.rb").to_s]
60
+ contains_admin_suite_portals =
61
+ portal_files.any? do |file|
62
+ content = File.binread(file).encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
63
+ content.match?(/(::)?AdminSuite\s*\.\s*portal\b/)
64
+ rescue StandardError
65
+ false
66
+ end
67
+
68
+ loader.ignore(admin_portals_dir) if contains_admin_suite_portals
48
69
  end
49
70
  end
50
71
 
@@ -59,7 +80,7 @@ module AdminSuite
59
80
 
60
81
  # Create loader and simulate initializer logic
61
82
  app_root = Pathname.new(@temp_dir)
62
- loader, ignored_dirs = create_tracked_loader
83
+ loader, ignored_dirs, _pushed_dirs = create_tracked_loader
63
84
  simulate_zeitwerk_integration(app_root, loader)
64
85
 
65
86
  # Verify that app/admin/portals was ignored
@@ -79,7 +100,7 @@ module AdminSuite
79
100
 
80
101
  # Create loader and simulate initializer logic
81
102
  app_root = Pathname.new(@temp_dir)
82
- loader, ignored_dirs = create_tracked_loader
103
+ loader, ignored_dirs, _pushed_dirs = create_tracked_loader
83
104
  simulate_zeitwerk_integration(app_root, loader)
84
105
 
85
106
  # Verify that app/admin/portals was NOT ignored
@@ -99,7 +120,7 @@ module AdminSuite
99
120
 
100
121
  # Create loader and simulate initializer logic
101
122
  app_root = Pathname.new(@temp_dir)
102
- loader, ignored_dirs = create_tracked_loader
123
+ loader, ignored_dirs, _pushed_dirs = create_tracked_loader
103
124
  simulate_zeitwerk_integration(app_root, loader)
104
125
 
105
126
  # Verify that app/admin_suite was ignored
@@ -127,7 +148,7 @@ module AdminSuite
127
148
 
128
149
  # Create loader and simulate initializer logic
129
150
  app_root = Pathname.new(@temp_dir)
130
- loader, ignored_dirs = create_tracked_loader
151
+ loader, ignored_dirs, _pushed_dirs = create_tracked_loader
131
152
  simulate_zeitwerk_integration(app_root, loader)
132
153
 
133
154
  # Verify that app/admin/portals was ignored due to presence of portal DSL
@@ -145,7 +166,7 @@ module AdminSuite
145
166
 
146
167
  # Create loader
147
168
  app_root = Pathname.new(@temp_dir)
148
- loader, ignored_dirs = create_tracked_loader
169
+ loader, ignored_dirs, _pushed_dirs = create_tracked_loader
149
170
 
150
171
  # Temporarily override File.binread to simulate read errors
151
172
  original_binread = File.singleton_class.instance_method(:binread)
@@ -171,5 +192,37 @@ module AdminSuite
171
192
  File.singleton_class.define_method(:binread, original_binread)
172
193
  end
173
194
  end
195
+
196
+ test "maps app/admin to Admin namespace when files define Admin constants" do
197
+ resources_dir = File.join(@temp_dir, "app", "admin", "resources")
198
+ FileUtils.mkdir_p(resources_dir)
199
+ File.write(
200
+ File.join(resources_dir, "user_resource.rb"),
201
+ "module Admin\n module Resources\n class UserResource; end\n end\nend\n"
202
+ )
203
+
204
+ app_root = Pathname.new(@temp_dir)
205
+ loader, _ignored_dirs, pushed_dirs = create_tracked_loader
206
+ simulate_zeitwerk_integration(app_root, loader)
207
+
208
+ assert pushed_dirs.any? { |h| h[:path] == app_root.join("app/admin").to_s && h[:namespace] == Admin },
209
+ "Expected app/admin to be pushed with namespace Admin when files define Admin::* constants"
210
+ end
211
+
212
+ test "does not map app/admin when it contains only top-level constants" do
213
+ resources_dir = File.join(@temp_dir, "app", "admin", "resources")
214
+ FileUtils.mkdir_p(resources_dir)
215
+ File.write(
216
+ File.join(resources_dir, "user_resource.rb"),
217
+ "module Resources\n class UserResource; end\nend\n"
218
+ )
219
+
220
+ app_root = Pathname.new(@temp_dir)
221
+ loader, _ignored_dirs, pushed_dirs = create_tracked_loader
222
+ simulate_zeitwerk_integration(app_root, loader)
223
+
224
+ assert pushed_dirs.none? { |h| h[:path] == app_root.join("app/admin").to_s },
225
+ "Expected app/admin to NOT be pushed when files contain only top-level constants"
226
+ end
174
227
  end
175
228
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: admin_suite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - TechWright Labs
@@ -237,6 +237,7 @@ files:
237
237
  - test/dummy/config/puma.rb
238
238
  - test/dummy/config/routes.rb
239
239
  - test/dummy/db/seeds.rb
240
+ - test/dummy/log/test.log
240
241
  - test/dummy/public/400.html
241
242
  - test/dummy/public/404.html
242
243
  - test/dummy/public/406-unsupported-browser.html
@@ -246,10 +247,12 @@ files:
246
247
  - test/dummy/public/icon.svg
247
248
  - test/dummy/public/robots.txt
248
249
  - test/dummy/test/test_helper.rb
250
+ - test/dummy/tmp/local_secret.txt
249
251
  - test/fixtures/docs/progress/PROGRESS_REPORT.md
250
252
  - test/integration/dashboard_test.rb
251
253
  - test/integration/docs_test.rb
252
254
  - test/integration/theme_test.rb
255
+ - test/lib/action_executor_test.rb
253
256
  - test/lib/markdown_renderer_test.rb
254
257
  - test/lib/theme_palette_test.rb
255
258
  - test/lib/zeitwerk_integration_test.rb