admin_suite 0.4.0 → 0.5.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +134 -7
  3. data/app/assets/vendor/chart.umd.min.js +14 -0
  4. data/app/controllers/admin_suite/resources_controller.rb +130 -5
  5. data/app/helpers/admin_suite/base_helper.rb +187 -10
  6. data/app/javascript/admin_suite_application.js +3 -0
  7. data/app/javascript/controllers/admin_suite/chart_controller.js +173 -0
  8. data/app/views/admin_suite/panels/_chart.html.erb +158 -18
  9. data/app/views/admin_suite/resources/index.html.erb +24 -5
  10. data/app/views/admin_suite/shared/_sidebar.html.erb +14 -8
  11. data/app/views/layouts/admin_suite/application.html.erb +6 -0
  12. data/config/routes.rb +3 -0
  13. data/lib/admin/base/filter_builder.rb +48 -5
  14. data/lib/admin/base/resource.rb +43 -7
  15. data/lib/admin_suite/configuration.rb +10 -6
  16. data/lib/admin_suite/engine.rb +1 -1
  17. data/lib/admin_suite/legacy_custom_renderer_procs.rb +1 -1
  18. data/lib/admin_suite/renderer_registry.rb +11 -0
  19. data/lib/admin_suite/renderers/legacy_gleania.rb +7 -5
  20. data/lib/admin_suite/ui/dashboard_definition.rb +6 -0
  21. data/lib/admin_suite/version.rb +1 -1
  22. data/lib/admin_suite.rb +13 -2
  23. data/test/integration/association_linking_test.rb +292 -0
  24. data/test/integration/chart_panel_test.rb +491 -0
  25. data/test/integration/dashboard_test.rb +9 -2
  26. data/test/integration/index_table_test.rb +289 -0
  27. data/test/integration/navigation_sections_test.rb +21 -0
  28. data/test/integration/searchable_select_search_test.rb +368 -0
  29. data/test/integration/show_hide_blank_test.rb +195 -0
  30. data/test/integration/toggle_test.rb +106 -0
  31. data/test/lib/definition_loader_test.rb +12 -0
  32. data/test/lib/form_field_renderer_test.rb +83 -11
  33. data/test/lib/format_table_cell_test.rb +49 -0
  34. data/test/lib/index_includes_test.rb +223 -0
  35. data/test/lib/legacy_renderer_deprecation_test.rb +8 -1
  36. data/test/lib/renderer_test.rb +23 -7
  37. data/test/lib/resource_exportable_deprecation_test.rb +10 -2
  38. data/test/test_helper.rb +47 -8
  39. metadata +35 -5
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ # Fixtures for the `includes:` DSL option (Task 4, phase 2b). A scope that
6
+ # genuinely supports `#includes` and records/validates the call, so the
7
+ # controller's "apply when the scope responds" and "degrade when
8
+ # `.includes` itself raises" paths are both exercisable end-to-end. The
9
+ # `LinkingFixtures::Deal`/`ReadOnlyResourceFixtures::Relation` pair (no
10
+ # `#includes` at all) already covers the "skip silently" path -- reused
11
+ # below rather than duplicated.
12
+ module IndexIncludesFixtures
13
+ class IncludesCapableRelation
14
+ include Enumerable
15
+
16
+ KNOWN_ASSOCIATIONS = %i[company owner].freeze
17
+
18
+ attr_reader :includes_calls
19
+
20
+ def initialize(records)
21
+ @records = records
22
+ @includes_calls = []
23
+ end
24
+
25
+ def each(&block) = @records.each(&block)
26
+ def count(*) = @records.count
27
+ def offset(*) = self
28
+ def limit(*) = self
29
+
30
+ # Mirrors real ActiveRecord's eventual `ActiveRecord::AssociationNotFoundError`
31
+ # for an unknown association name -- raised here at call time (rather
32
+ # than lazily, as real AR does) so the controller's rescue path around
33
+ # the `.includes` call site is directly exercisable.
34
+ def includes(*associations)
35
+ flat = associations.flatten
36
+ invalid = flat.reject { |a| KNOWN_ASSOCIATIONS.include?(a.respond_to?(:to_sym) ? a.to_sym : a) }
37
+ raise ArgumentError, "Association named #{invalid.first.inspect} was not found" if invalid.any?
38
+
39
+ @includes_calls << flat
40
+ self
41
+ end
42
+ end
43
+
44
+ class Widget
45
+ extend ActiveModel::Naming
46
+
47
+ attr_reader :id, :name
48
+
49
+ def initialize(id:, name:)
50
+ @id = id
51
+ @name = name
52
+ end
53
+
54
+ RELATION = IncludesCapableRelation.new([ new(id: 1, name: "Widget One") ])
55
+
56
+ def self.all = RELATION
57
+ def self.column_names = %w[id name]
58
+ def self.primary_key = "id"
59
+ def self.columns_hash = { "id" => Struct.new(:type).new(:integer) }
60
+ def self.find(id) = all.find { |w| w.to_param == id.to_s }
61
+ def to_param = id.to_s
62
+ def attributes = { "id" => id, "name" => name }
63
+ end
64
+
65
+ class BadWidget
66
+ extend ActiveModel::Naming
67
+
68
+ attr_reader :id, :name
69
+
70
+ def initialize(id:, name:)
71
+ @id = id
72
+ @name = name
73
+ end
74
+
75
+ RELATION = IncludesCapableRelation.new([ new(id: 1, name: "Bad Widget") ])
76
+
77
+ def self.all = RELATION
78
+ def self.column_names = %w[id name]
79
+ def self.primary_key = "id"
80
+ def self.columns_hash = { "id" => Struct.new(:type).new(:integer) }
81
+ def self.find(id) = all.find { |w| w.to_param == id.to_s }
82
+ def to_param = id.to_s
83
+ def attributes = { "id" => id, "name" => name }
84
+ end
85
+
86
+ # Self-contained (this file is runnable standalone via
87
+ # `rake test TEST=test/lib/index_includes_test.rb`, so it can't reach
88
+ # into fixtures defined only in association_linking_test.rb): a scope
89
+ # with no `#includes` method at all, mirroring
90
+ # `ReadOnlyResourceFixtures::Relation`, backing a resource that
91
+ # nonetheless declares `includes :company`. Exercises the "skip
92
+ # silently, don't raise" path.
93
+ class NoIncludesWidget
94
+ extend ActiveModel::Naming
95
+
96
+ attr_reader :id, :name
97
+
98
+ def initialize(id:, name:)
99
+ @id = id
100
+ @name = name
101
+ end
102
+
103
+ def self.all = ReadOnlyResourceFixtures::Relation.new([ new(id: 1, name: "Plain Widget") ])
104
+ def self.column_names = %w[id name]
105
+ def self.primary_key = "id"
106
+ def self.columns_hash = { "id" => Struct.new(:type).new(:integer) }
107
+ def self.find(id) = all.find { |w| w.to_param == id.to_s }
108
+ def to_param = id.to_s
109
+ def attributes = { "id" => id, "name" => name }
110
+ end
111
+ end
112
+
113
+ module Admin
114
+ module Resources
115
+ class IndexIncludesWidgetResource < Admin::Base::Resource
116
+ model IndexIncludesFixtures::Widget
117
+ portal :ops
118
+ section :observability
119
+
120
+ index do
121
+ columns { column :name }
122
+ includes :company, :owner
123
+ end
124
+ end
125
+
126
+ class IndexIncludesBadWidgetResource < Admin::Base::Resource
127
+ model IndexIncludesFixtures::BadWidget
128
+ portal :ops
129
+ section :observability
130
+
131
+ index do
132
+ columns { column :name }
133
+ includes :nonexistent_association
134
+ end
135
+ end
136
+
137
+ class IndexIncludesNoIncludesWidgetResource < Admin::Base::Resource
138
+ model IndexIncludesFixtures::NoIncludesWidget
139
+ portal :ops
140
+ section :observability
141
+
142
+ index do
143
+ columns { column :name }
144
+ includes :company
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ module AdminSuite
151
+ class IndexIncludesTest < ActionDispatch::IntegrationTest
152
+ test "IndexConfig#includes stores the list of associations" do
153
+ config = Admin::Base::Resource::IndexConfig.new
154
+ config.includes(:company, :owner)
155
+ assert_equal [ :company, :owner ], config.includes_list
156
+ end
157
+
158
+ test "IndexConfig#includes defaults to an empty list" do
159
+ assert_equal [], Admin::Base::Resource::IndexConfig.new.includes_list
160
+ end
161
+
162
+ test "IndexConfig#includes drops a nil association instead of storing garbage" do
163
+ config = Admin::Base::Resource::IndexConfig.new
164
+ config.includes(nil)
165
+ assert_equal [], config.includes_list
166
+ end
167
+
168
+ test "IndexConfig#includes tolerates a String association name" do
169
+ config = Admin::Base::Resource::IndexConfig.new
170
+ config.includes("company")
171
+ assert_equal [ "company" ], config.includes_list
172
+ end
173
+
174
+ test "the DSL exposes includes: on a resource's index_config" do
175
+ assert_equal [ :company, :owner ], Admin::Resources::IndexIncludesWidgetResource.index_config.includes_list
176
+ end
177
+
178
+ test "the controller applies includes: to a scope that responds to includes" do
179
+ IndexIncludesFixtures::Widget::RELATION.includes_calls.clear
180
+
181
+ get "/internal/admin_suite/ops/index_includes_widgets"
182
+ assert_response :success
183
+
184
+ assert_equal [ [ :company, :owner ] ], IndexIncludesFixtures::Widget::RELATION.includes_calls
185
+ end
186
+
187
+ test "a scope whose includes raises for a bad association degrades instead of 500ing" do
188
+ logged = []
189
+ Rails.logger.stub(:warn, ->(msg) { logged << msg }) do
190
+ get "/internal/admin_suite/ops/index_includes_bad_widgets"
191
+ end
192
+
193
+ assert_response :success
194
+ assert_includes response.body, "Bad Widget"
195
+ # The rescue in `apply_index_includes` logs the failure -- pinning
196
+ # that here so the "skipped silently" test below (which asserts NO
197
+ # warn fires) is actually distinguishing two different code paths,
198
+ # not just two tests that both happen to pass.
199
+ assert_equal 1, logged.size
200
+ assert_includes logged.first, "nonexistent_association"
201
+ end
202
+
203
+ test "a scope that does not respond to includes is skipped silently, not raised" do
204
+ refute IndexIncludesFixtures::NoIncludesWidget.all.respond_to?(:includes)
205
+
206
+ logged = []
207
+ Rails.logger.stub(:warn, ->(msg) { logged << msg }) do
208
+ get "/internal/admin_suite/ops/index_includes_no_includes_widgets"
209
+ end
210
+
211
+ assert_response :success
212
+ assert_includes response.body, "Plain Widget"
213
+ # `apply_index_includes`'s `respond_to?(:includes)` guard exists so a
214
+ # scope that can never support includes doesn't spam a warning on
215
+ # every request. Deleting that guard would still return a 200 here
216
+ # (the catch-all rescue produces an identical response), so the
217
+ # response alone doesn't prove the guard does anything -- this
218
+ # asserts its actual purpose: no warn fires on this path. Contrast
219
+ # with the bad-association test above, where one does.
220
+ assert_empty logged
221
+ end
222
+ end
223
+ end
@@ -24,7 +24,14 @@ module AdminSuite
24
24
  end
25
25
  assert_equal 1, logged.size
26
26
  assert_includes logged.first, "deprecated"
27
- assert_includes logged.first, "0.5.0"
27
+ # Deliberately a hardcoded literal, not read off
28
+ # DEPRECATION_MESSAGE_FORMAT: reading the version out of the constant
29
+ # under test and asserting the message contains it is true by
30
+ # construction (format() only substitutes %<key>s, never the version
31
+ # digits), so it can never fail on a wrong retarget -- exactly the
32
+ # property this test exists to catch. If the removal target changes
33
+ # again, this literal must be updated by hand; that's the point.
34
+ assert_includes logged.first, "0.6.0"
28
35
  end
29
36
 
30
37
  test "the gem no longer includes the host CustomRenderersHelper constant" do
@@ -116,7 +116,14 @@ module AdminSuite
116
116
  assert_equal 1, logged.size
117
117
  assert_includes logged.first, key.to_s
118
118
  assert_includes logged.first, "deprecated"
119
- assert_includes logged.first, "0.5.0"
119
+ # Deliberately a hardcoded literal, not read off
120
+ # DEPRECATION_MESSAGE_FORMAT: reading the version out of the constant
121
+ # under test and asserting the message contains it is true by
122
+ # construction (format() only substitutes %<key>s, never the version
123
+ # digits), so it can never fail on a wrong retarget -- exactly the
124
+ # property this test exists to catch. If the removal target changes
125
+ # again, this literal must be updated by hand; that's the point.
126
+ assert_includes logged.first, "0.6.0"
120
127
  ensure
121
128
  AdminSuite.config.custom_renderers.delete(key)
122
129
  end
@@ -144,6 +151,12 @@ module AdminSuite
144
151
  assert_includes html, "host-class-7"
145
152
  refute_includes html, "Hello 7"
146
153
  ensure
154
+ # `register_default` seeds AdminSuite's *permanent* default store (the
155
+ # same one the built-ins live in), so this probe key must be removed
156
+ # via `unregister_default` -- unlike `unregister`, which only ever
157
+ # touches the explicit store and would silently leave this key behind
158
+ # for every test that runs afterward.
159
+ AdminSuite::RendererRegistry.unregister_default(key)
147
160
  if Admin::Renderers.const_defined?(:HostBeatsDefaultProbeRenderer, false)
148
161
  Admin::Renderers.send(:remove_const, :HostBeatsDefaultProbeRenderer)
149
162
  end
@@ -172,12 +185,15 @@ module AdminSuite
172
185
  test "a registered renderer class wins over the built-in case branches" do
173
186
  # :json_preview has a legacy case branch; a registered class must take it over.
174
187
  #
175
- # Task 4 registers :json_preview as a permanent alias to JsonRenderer
176
- # at require time, so unlike when this test was first written —
177
- # there is now always a prior registration to restore. Unregistering
178
- # unconditionally in `ensure` would delete that alias process-wide for
179
- # every test that runs after this one (a real, order-dependent bug
180
- # this exact test used to hide).
188
+ # Task 4 registers :json_preview as a permanent alias to JsonRenderer,
189
+ # but via `register_default` (the gem-defaults store), not `register`
190
+ # (the explicit store). `lookup` below only ever reads the explicit
191
+ # store, so `previous` is nil here -- there is no explicit registration
192
+ # to save or restore, and the `ensure` branch's plain `unregister` is
193
+ # a safe no-op that cannot touch the permanent default alias (which
194
+ # only `unregister_default` could remove). This test still needs the
195
+ # save/restore shape below in case some future test registers
196
+ # :json_preview explicitly and forgets to clean up after itself.
181
197
  previous = AdminSuite::RendererRegistry.lookup(:json_preview)
182
198
  AdminSuite::RendererRegistry.register(:json_preview, GreetingRenderer)
183
199
  assert_includes render_custom_section(Record.new(6), :json_preview), "Hello 6"
@@ -25,7 +25,7 @@ module AdminSuite
25
25
  refute LegacyExportableResource.instance_variable_defined?(:@export_formats)
26
26
  end
27
27
 
28
- test "exportable warns once per resource class, naming the class and the 0.5.0 removal" do
28
+ test "exportable warns once per resource class, naming the class and the 0.6.0 removal" do
29
29
  logged = []
30
30
  LegacyExportableResource.stub(:warn_once_sink, ->(msg) { logged << msg }) do
31
31
  2.times { LegacyExportableResource.exportable(:json, :csv) }
@@ -33,7 +33,15 @@ module AdminSuite
33
33
 
34
34
  assert_equal 1, logged.size
35
35
  assert_includes logged.first, "LegacyExportableResource"
36
- assert_includes logged.first, "0.5.0"
36
+ # Deliberately a hardcoded literal, not read off
37
+ # EXPORTABLE_DEPRECATION_MESSAGE_FORMAT: reading the version out of
38
+ # the constant under test and asserting the message contains it is
39
+ # true by construction (format() only substitutes %<resource>s, never
40
+ # the version digits), so it can never fail on a wrong retarget --
41
+ # exactly the property this test exists to catch. If the removal
42
+ # target changes again, this literal must be updated by hand; that's
43
+ # the point.
44
+ assert_includes logged.first, "0.6.0"
37
45
  end
38
46
  end
39
47
  end
data/test/test_helper.rb CHANGED
@@ -43,14 +43,6 @@ unless defined?(ActiveRecord::Base)
43
43
  end
44
44
  end
45
45
 
46
- module TurboFrameTestHelper
47
- def turbo_frame_tag(name, **options, &block)
48
- content_tag(:turbo_frame, capture(&block), id: name, **options)
49
- end
50
- end
51
-
52
- ActionView::Base.include(TurboFrameTestHelper)
53
-
54
46
  module ReadOnlyResourceFixtures
55
47
  class Relation
56
48
  include Enumerable
@@ -142,3 +134,50 @@ module Admin
142
134
  end
143
135
  end
144
136
  end
137
+
138
+ # The dummy app is database-free and `ActiveRecord::Base` above is a bare
139
+ # stub class, so every other fixture in this suite is a PORO and
140
+ # `active_record_base?` is false for all of them -- every AR-dependent path
141
+ # (`auto_admin_suite_path_for`, `format_table_cell`'s AR branch) is
142
+ # otherwise unreachable by tests. Subclassing the stub gives a fixture that
143
+ # genuinely satisfies `active_record_base?`, with no database and no
144
+ # railtie required. Established here (Task 3, phase 2b) because it's
145
+ # reused by later tasks (4, 7); those tasks' own test files each `require
146
+ # "test_helper"`, so this is visible to them even when a single test file
147
+ # is run in isolation via `rake test TEST=...` -- unlike a fixture defined
148
+ # only in association_linking_test.rb, which would not be.
149
+ module LinkingFixtures
150
+ class Company < ActiveRecord::Base
151
+ extend ActiveModel::Naming
152
+
153
+ attr_reader :id, :name
154
+
155
+ def initialize(id: 7, name: "Acme Corp")
156
+ @id = id
157
+ @name = name
158
+ end
159
+
160
+ def self.all = ReadOnlyResourceFixtures::Relation.new([ new ])
161
+ def self.column_names = %w[id name]
162
+ def self.primary_key = "id"
163
+ def self.columns_hash = { "id" => Struct.new(:type).new(:integer) }
164
+ def self.find(_id) = new
165
+ def to_param = id.to_s
166
+ def attributes = { "id" => id, "name" => name }
167
+ end
168
+ end
169
+
170
+ module Admin
171
+ module Resources
172
+ # Registered so `auto_admin_suite_path_for` can resolve a real path for
173
+ # `LinkingFixtures::Company` instances (it looks up `registered_resources`
174
+ # by `model_class`). Never call `Admin::Base::Resource.reset_registry!` in
175
+ # a test: `inherited` fires only on class creation, so a reset-then-reload
176
+ # would permanently empty the registry for the rest of the run.
177
+ class LinkingCompanyResource < Admin::Base::Resource
178
+ model LinkingFixtures::Company
179
+ portal :ops
180
+ section :observability
181
+ end
182
+ end
183
+ 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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TechWright Labs
@@ -36,20 +36,40 @@ dependencies:
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '6.0'
39
+ version: '9.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '10'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '9.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '10'
53
+ - !ruby/object:Gem::Dependency
54
+ name: turbo-rails
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '2.0'
40
60
  - - "<"
41
61
  - !ruby/object:Gem::Version
42
- version: '11.0'
62
+ version: '3.0'
43
63
  type: :runtime
44
64
  prerelease: false
45
65
  version_requirements: !ruby/object:Gem::Requirement
46
66
  requirements:
47
67
  - - ">="
48
68
  - !ruby/object:Gem::Version
49
- version: '6.0'
69
+ version: '2.0'
50
70
  - - "<"
51
71
  - !ruby/object:Gem::Version
52
- version: '11.0'
72
+ version: '3.0'
53
73
  - !ruby/object:Gem::Dependency
54
74
  name: lucide-rails
55
75
  requirement: !ruby/object:Gem::Requirement
@@ -126,6 +146,7 @@ files:
126
146
  - app/assets/builds/admin_suite_tailwind.css
127
147
  - app/assets/rouge.css
128
148
  - app/assets/tailwind/admin_suite.css
149
+ - app/assets/vendor/chart.umd.min.js
129
150
  - app/assets/vendor/easymde.min.css
130
151
  - app/assets/vendor/easymde.min.js
131
152
  - app/controllers/admin_suite/application_controller.rb
@@ -139,6 +160,7 @@ files:
139
160
  - app/helpers/admin_suite/resources_helper.rb
140
161
  - app/helpers/admin_suite/theme_helper.rb
141
162
  - app/javascript/admin_suite_application.js
163
+ - app/javascript/controllers/admin_suite/chart_controller.js
142
164
  - app/javascript/controllers/admin_suite/click_actions_controller.js
143
165
  - app/javascript/controllers/admin_suite/clipboard_controller.js
144
166
  - app/javascript/controllers/admin_suite/code_editor_controller.js
@@ -260,15 +282,21 @@ files:
260
282
  - test/dummy/public/robots.txt
261
283
  - test/dummy/test/test_helper.rb
262
284
  - test/fixtures/docs/progress/PROGRESS_REPORT.md
285
+ - test/integration/association_linking_test.rb
263
286
  - test/integration/authentication_test.rb
264
287
  - test/integration/authorization_test.rb
288
+ - test/integration/chart_panel_test.rb
265
289
  - test/integration/dashboard_test.rb
266
290
  - test/integration/docs_test.rb
291
+ - test/integration/index_table_test.rb
267
292
  - test/integration/layout_assets_test.rb
268
293
  - test/integration/navigation_sections_test.rb
269
294
  - test/integration/pagination_and_stats_test.rb
270
295
  - test/integration/read_only_resource_test.rb
296
+ - test/integration/searchable_select_search_test.rb
297
+ - test/integration/show_hide_blank_test.rb
271
298
  - test/integration/theme_test.rb
299
+ - test/integration/toggle_test.rb
272
300
  - test/lib/action_executor_redirect_test.rb
273
301
  - test/lib/action_executor_test.rb
274
302
  - test/lib/auth_http_basic_test.rb
@@ -278,6 +306,8 @@ files:
278
306
  - test/lib/definition_loader_test.rb
279
307
  - test/lib/engine_defaults_test.rb
280
308
  - test/lib/form_field_renderer_test.rb
309
+ - test/lib/format_table_cell_test.rb
310
+ - test/lib/index_includes_test.rb
281
311
  - test/lib/legacy_renderer_deprecation_test.rb
282
312
  - test/lib/markdown_renderer_test.rb
283
313
  - test/lib/renderer_test.rb