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.
- checksums.yaml +4 -4
- data/app/assets/admin_suite.css +128 -0
- data/app/controllers/admin_suite/application_controller.rb +32 -2
- data/app/controllers/admin_suite/dashboard_controller.rb +59 -226
- data/app/helpers/admin_suite/base_helper.rb +108 -108
- data/app/helpers/admin_suite/panels_helper.rb +1 -1
- data/app/javascript/controllers/admin_suite/file_upload_controller.js +9 -9
- data/app/javascript/controllers/admin_suite/json_editor_controller.js +8 -8
- data/app/javascript/controllers/admin_suite/searchable_select_controller.js +2 -2
- data/app/javascript/controllers/admin_suite/tag_select_controller.js +1 -1
- data/app/javascript/controllers/admin_suite/toggle_switch_controller.js +1 -1
- data/app/views/admin_suite/dashboard/index.html.erb +6 -15
- data/app/views/admin_suite/panels/_cards.html.erb +6 -6
- data/app/views/admin_suite/panels/_chart.html.erb +12 -12
- data/app/views/admin_suite/panels/_health.html.erb +14 -14
- data/app/views/admin_suite/panels/_recent.html.erb +11 -11
- data/app/views/admin_suite/panels/_stat.html.erb +24 -24
- data/app/views/admin_suite/panels/_table.html.erb +10 -10
- data/app/views/admin_suite/portals/show.html.erb +1 -1
- data/app/views/admin_suite/resources/_form.html.erb +1 -1
- data/app/views/admin_suite/resources/edit.html.erb +4 -4
- data/app/views/admin_suite/resources/index.html.erb +23 -23
- data/app/views/admin_suite/resources/new.html.erb +4 -4
- data/app/views/admin_suite/resources/show.html.erb +17 -17
- data/app/views/admin_suite/shared/_form.html.erb +8 -8
- data/app/views/admin_suite/shared/_json_editor_field.html.erb +4 -4
- data/app/views/admin_suite/shared/_sidebar.html.erb +4 -4
- data/app/views/admin_suite/shared/_topbar.html.erb +1 -1
- data/app/views/layouts/admin_suite/application.html.erb +4 -4
- data/docs/configuration.md +56 -6
- data/docs/portals.md +42 -0
- data/lib/admin/base/action_executor.rb +69 -0
- data/lib/admin_suite/configuration.rb +12 -0
- data/lib/admin_suite/engine.rb +82 -31
- data/lib/admin_suite/ui/field_renderer_registry.rb +2 -2
- data/lib/admin_suite/ui/form_field_renderer.rb +2 -2
- data/lib/admin_suite/ui/show_formatter_registry.rb +5 -5
- data/lib/admin_suite/ui/show_value_formatter.rb +1 -1
- data/lib/admin_suite/version.rb +1 -1
- data/lib/admin_suite.rb +31 -0
- data/lib/generators/admin_suite/install/templates/admin_suite.rb +8 -0
- data/test/dummy/log/test.log +1512 -0
- data/test/dummy/tmp/local_secret.txt +1 -0
- data/test/integration/dashboard_test.rb +57 -1
- data/test/lib/action_executor_test.rb +172 -0
- data/test/lib/zeitwerk_integration_test.rb +69 -16
- metadata +4 -1
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,
|
|
@@ -99,6 +141,15 @@ module AdminSuite
|
|
|
99
141
|
]
|
|
100
142
|
end
|
|
101
143
|
|
|
144
|
+
if config.dashboard_globs.blank?
|
|
145
|
+
config.dashboard_globs = [
|
|
146
|
+
Rails.root.join("config/admin_suite/dashboard.rb").to_s,
|
|
147
|
+
Rails.root.join("config/admin_suite/dashboard/*.rb").to_s,
|
|
148
|
+
Rails.root.join("app/admin_suite/dashboard.rb").to_s,
|
|
149
|
+
Rails.root.join("app/admin_suite/dashboard/*.rb").to_s
|
|
150
|
+
]
|
|
151
|
+
end
|
|
152
|
+
|
|
102
153
|
config.portals = {
|
|
103
154
|
ops: { label: "Ops Portal", icon: "settings", color: :amber, order: 10 },
|
|
104
155
|
email: { label: "Email Portal", icon: "inbox", color: :emerald, order: 20 },
|
|
@@ -75,11 +75,11 @@ AdminSuite::UI::FieldRendererRegistry.register(:attachment) do |view, f, field,
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
AdminSuite::UI::FieldRendererRegistry.register(:trix) do |_view, f, field, resource, _field_class|
|
|
78
|
-
f.rich_text_area(field.name, class: "prose
|
|
78
|
+
f.rich_text_area(field.name, class: "prose max-w-none")
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
AdminSuite::UI::FieldRendererRegistry.register(:rich_text) do |_view, f, field, resource, _field_class|
|
|
82
|
-
f.rich_text_area(field.name, class: "prose
|
|
82
|
+
f.rich_text_area(field.name, class: "prose max-w-none")
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
AdminSuite::UI::FieldRendererRegistry.register(:markdown) do |_view, f, field, resource, field_class|
|
|
@@ -38,8 +38,8 @@ module AdminSuite
|
|
|
38
38
|
|
|
39
39
|
concat(field_html)
|
|
40
40
|
|
|
41
|
-
concat(content_tag(:p, field.help, class: "mt-1 text-sm text-slate-500
|
|
42
|
-
concat(content_tag(:p, resource.errors[field.name].first, class: "mt-1 text-sm text-red-600
|
|
41
|
+
concat(content_tag(:p, field.help, class: "mt-1 text-sm text-slate-500")) if field.help.present?
|
|
42
|
+
concat(content_tag(:p, resource.errors[field.name].first, class: "mt-1 text-sm text-red-600")) if resource.errors[field.name].any?
|
|
43
43
|
end)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -40,7 +40,7 @@ end
|
|
|
40
40
|
AdminSuite::UI::ShowFormatterRegistry.register_class(TrueClass) do |_value, view, _record, _field|
|
|
41
41
|
view.content_tag(:span, class: "inline-flex items-center gap-1") do
|
|
42
42
|
view.concat(view.admin_suite_icon("check-circle-2", class: "w-4 h-4 text-green-500"))
|
|
43
|
-
view.concat(view.content_tag(:span, "Yes", class: "text-green-600
|
|
43
|
+
view.concat(view.content_tag(:span, "Yes", class: "text-green-600 font-medium"))
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -54,14 +54,14 @@ end
|
|
|
54
54
|
AdminSuite::UI::ShowFormatterRegistry.register_class(Time) do |value, view, _record, _field|
|
|
55
55
|
view.content_tag(:span, class: "inline-flex items-center gap-2") do
|
|
56
56
|
view.concat(view.content_tag(:span, value.strftime("%B %d, %Y at %H:%M"), class: "font-medium"))
|
|
57
|
-
view.concat(view.content_tag(:span, "(#{view.time_ago_in_words(value)} ago)", class: "text-slate-500
|
|
57
|
+
view.concat(view.content_tag(:span, "(#{view.time_ago_in_words(value)} ago)", class: "text-slate-500 text-xs"))
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
AdminSuite::UI::ShowFormatterRegistry.register_class(DateTime) do |value, view, _record, _field|
|
|
62
62
|
view.content_tag(:span, class: "inline-flex items-center gap-2") do
|
|
63
63
|
view.concat(view.content_tag(:span, value.strftime("%B %d, %Y at %H:%M"), class: "font-medium"))
|
|
64
|
-
view.concat(view.content_tag(:span, "(#{view.time_ago_in_words(value)} ago)", class: "text-slate-500
|
|
64
|
+
view.concat(view.content_tag(:span, "(#{view.time_ago_in_words(value)} ago)", class: "text-slate-500 text-xs"))
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -72,7 +72,7 @@ end
|
|
|
72
72
|
if defined?(ActiveRecord::Base)
|
|
73
73
|
AdminSuite::UI::ShowFormatterRegistry.register_class(ActiveRecord::Base) do |value, view, _record, _field|
|
|
74
74
|
link_text = value.respond_to?(:name) ? value.name : "#{value.class.name} ##{value.id}"
|
|
75
|
-
view.content_tag(:span, link_text, class: "text-indigo-600
|
|
75
|
+
view.content_tag(:span, link_text, class: "text-indigo-600")
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -88,7 +88,7 @@ AdminSuite::UI::ShowFormatterRegistry.register_class(Array) do |value, view, _re
|
|
|
88
88
|
else
|
|
89
89
|
view.content_tag(:div, class: "flex flex-wrap gap-1") do
|
|
90
90
|
value.each do |item|
|
|
91
|
-
view.concat(view.content_tag(:span, item.to_s, class: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100
|
|
91
|
+
view.concat(view.content_tag(:span, item.to_s, class: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-700"))
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
end
|
data/lib/admin_suite/version.rb
CHANGED
data/lib/admin_suite.rb
CHANGED
|
@@ -53,5 +53,36 @@ module AdminSuite
|
|
|
53
53
|
def portal_definitions
|
|
54
54
|
PortalRegistry.all
|
|
55
55
|
end
|
|
56
|
+
|
|
57
|
+
# Defines (or updates) the root dashboard shown at the engine root (`/`).
|
|
58
|
+
#
|
|
59
|
+
# This uses the same dashboard DSL as portal dashboards.
|
|
60
|
+
#
|
|
61
|
+
# Host apps typically place these in:
|
|
62
|
+
# - `config/admin_suite/dashboard.rb` (recommended; not a Zeitwerk autoload path)
|
|
63
|
+
# - `app/admin_suite/dashboard.rb` (supported; AdminSuite ignores for Zeitwerk)
|
|
64
|
+
#
|
|
65
|
+
# @yield Dashboard definition DSL
|
|
66
|
+
# @return [AdminSuite::UI::DashboardDefinition]
|
|
67
|
+
def root_dashboard(&block)
|
|
68
|
+
config.root_dashboard_definition ||= UI::DashboardDefinition.new
|
|
69
|
+
UI::DashboardDSL.new(config.root_dashboard_definition).instance_eval(&block) if block_given?
|
|
70
|
+
config.root_dashboard_definition
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [AdminSuite::UI::DashboardDefinition, nil]
|
|
74
|
+
def root_dashboard_definition
|
|
75
|
+
config.root_dashboard_definition
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Clears the root dashboard definition (useful for development reloads).
|
|
79
|
+
#
|
|
80
|
+
# @return [void]
|
|
81
|
+
def reset_root_dashboard!
|
|
82
|
+
config.root_dashboard_definition = nil
|
|
83
|
+
config.root_dashboard_loaded = false
|
|
84
|
+
rescue StandardError
|
|
85
|
+
# best-effort
|
|
86
|
+
end
|
|
56
87
|
end
|
|
57
88
|
end
|
|
@@ -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 = [
|