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
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
<% end %>
|
|
57
57
|
<% end %>
|
|
58
58
|
|
|
59
|
-
<div class="flex flex-col
|
|
59
|
+
<div class="admin-suite-two-col flex flex-col md:flex-row gap-6">
|
|
60
60
|
<!-- Filters Sidebar -->
|
|
61
61
|
<% if resource_config.index_config&.filters_list&.any? || resource_config.index_config&.searchable_fields&.any? %>
|
|
62
|
-
<div class="
|
|
62
|
+
<div class="admin-suite-two-col__sidebar admin-suite-two-col__sidebar--filters md:w-64 flex-shrink-0">
|
|
63
63
|
<div class="bg-white rounded-xl border border-slate-200 p-4">
|
|
64
|
-
<h3 class="font-medium text-slate-900
|
|
64
|
+
<h3 class="font-medium text-slate-900 mb-4">Filters</h3>
|
|
65
65
|
<%= form_with url: url_for(action: :index), method: :get,
|
|
66
66
|
data: {
|
|
67
67
|
turbo_frame: "resource_results",
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
<% end %>
|
|
137
137
|
|
|
138
138
|
<!-- Main Content -->
|
|
139
|
-
<div class="flex-1">
|
|
139
|
+
<div class="flex-1 min-w-0">
|
|
140
140
|
<%= turbo_frame_tag "resource_results", data: { turbo_action: "advance" } do %>
|
|
141
141
|
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
142
142
|
<% if @collection.any? %>
|
|
@@ -216,25 +216,25 @@
|
|
|
216
216
|
|
|
217
217
|
<!-- Pagination -->
|
|
218
218
|
<% if @pagy.pages > 1 %>
|
|
219
|
-
<div class="px-6 py-4 border-t border-slate-200
|
|
219
|
+
<div class="px-6 py-4 border-t border-slate-200">
|
|
220
220
|
<div class="flex items-center justify-between">
|
|
221
|
-
<div class="text-sm text-slate-500
|
|
222
|
-
Showing <span class="font-medium text-slate-700
|
|
223
|
-
<span class="font-medium text-slate-700
|
|
224
|
-
<span class="font-medium text-slate-700
|
|
221
|
+
<div class="text-sm text-slate-500">
|
|
222
|
+
Showing <span class="font-medium text-slate-700"><%= @pagy.from %></span> to
|
|
223
|
+
<span class="font-medium text-slate-700"><%= @pagy.to %></span> of
|
|
224
|
+
<span class="font-medium text-slate-700"><%= number_with_delimiter(@pagy.count) %></span> results
|
|
225
225
|
</div>
|
|
226
226
|
|
|
227
227
|
<nav class="flex items-center gap-1">
|
|
228
228
|
<% if @pagy.prev %>
|
|
229
229
|
<%= link_to url_for(params.permit!.merge(page: @pagy.prev)),
|
|
230
|
-
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700
|
|
230
|
+
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors" do %>
|
|
231
231
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
232
232
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
233
233
|
</svg>
|
|
234
234
|
Prev
|
|
235
235
|
<% end %>
|
|
236
236
|
<% else %>
|
|
237
|
-
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400
|
|
237
|
+
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed">
|
|
238
238
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
239
239
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
240
240
|
</svg>
|
|
@@ -245,10 +245,10 @@
|
|
|
245
245
|
<div class="hidden sm:flex items-center gap-1 mx-2">
|
|
246
246
|
<% @pagy.series.each do |item| %>
|
|
247
247
|
<% if item == :gap %>
|
|
248
|
-
<span class="px-2 py-1 text-sm text-slate-400
|
|
248
|
+
<span class="px-2 py-1 text-sm text-slate-400">…</span>
|
|
249
249
|
<% elsif item.is_a?(Integer) %>
|
|
250
250
|
<%= link_to item, url_for(params.permit!.merge(page: item)),
|
|
251
|
-
class: "px-3 py-1.5 text-sm font-medium text-slate-700
|
|
251
|
+
class: "px-3 py-1.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors" %>
|
|
252
252
|
<% elsif item.is_a?(String) %>
|
|
253
253
|
<span class="px-3 py-1.5 text-sm font-medium text-white rounded-lg" style="background: var(--admin-suite-primary); border: 1px solid var(--admin-suite-primary);">
|
|
254
254
|
<%= item %>
|
|
@@ -257,20 +257,20 @@
|
|
|
257
257
|
<% end %>
|
|
258
258
|
</div>
|
|
259
259
|
|
|
260
|
-
<span class="sm:hidden px-3 py-1.5 text-sm text-slate-500
|
|
260
|
+
<span class="sm:hidden px-3 py-1.5 text-sm text-slate-500">
|
|
261
261
|
Page <%= @pagy.page %> of <%= @pagy.pages %>
|
|
262
262
|
</span>
|
|
263
263
|
|
|
264
264
|
<% if @pagy.next %>
|
|
265
265
|
<%= link_to url_for(params.permit!.merge(page: @pagy.next)),
|
|
266
|
-
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700
|
|
266
|
+
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors" do %>
|
|
267
267
|
Next
|
|
268
268
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
269
269
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
270
270
|
</svg>
|
|
271
271
|
<% end %>
|
|
272
272
|
<% else %>
|
|
273
|
-
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400
|
|
273
|
+
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed">
|
|
274
274
|
Next
|
|
275
275
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
276
276
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
@@ -281,22 +281,22 @@
|
|
|
281
281
|
</div>
|
|
282
282
|
</div>
|
|
283
283
|
<% else %>
|
|
284
|
-
<div class="px-6 py-3 border-t border-slate-200
|
|
285
|
-
<div class="text-sm text-slate-500
|
|
286
|
-
Showing <span class="font-medium text-slate-700
|
|
284
|
+
<div class="px-6 py-3 border-t border-slate-200">
|
|
285
|
+
<div class="text-sm text-slate-500">
|
|
286
|
+
Showing <span class="font-medium text-slate-700"><%= @pagy.count %></span> results
|
|
287
287
|
</div>
|
|
288
288
|
</div>
|
|
289
289
|
<% end %>
|
|
290
290
|
<% else %>
|
|
291
291
|
<div class="p-12 text-center">
|
|
292
|
-
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-slate-100
|
|
292
|
+
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-slate-100 mb-4">
|
|
293
293
|
<svg class="w-6 h-6 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
294
294
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
|
|
295
295
|
</svg>
|
|
296
296
|
</div>
|
|
297
|
-
<h3 class="text-lg font-medium text-slate-900
|
|
297
|
+
<h3 class="text-lg font-medium text-slate-900">No <%= resource_config.human_name_plural.downcase %> found</h3>
|
|
298
298
|
<% if has_new_route %>
|
|
299
|
-
<p class="text-slate-500
|
|
299
|
+
<p class="text-slate-500 mt-1">Get started by creating your first one.</p>
|
|
300
300
|
<%= link_to url_for(action: :new),
|
|
301
301
|
class: theme_btn_primary_class + " mt-4",
|
|
302
302
|
data: { turbo_frame: "_top" } do %>
|
|
@@ -304,7 +304,7 @@
|
|
|
304
304
|
New <%= resource_config.human_name %>
|
|
305
305
|
<% end %>
|
|
306
306
|
<% else %>
|
|
307
|
-
<p class="text-slate-500
|
|
307
|
+
<p class="text-slate-500 mt-1">No records have been created yet.</p>
|
|
308
308
|
<% end %>
|
|
309
309
|
</div>
|
|
310
310
|
<% end %>
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
<div class="px-4 sm:px-6 lg:px-8 max-w-3xl mx-auto">
|
|
4
4
|
<!-- Header -->
|
|
5
5
|
<div class="mb-6">
|
|
6
|
-
<nav class="flex items-center gap-2 text-sm text-slate-500
|
|
6
|
+
<nav class="flex items-center gap-2 text-sm text-slate-500 mb-2">
|
|
7
7
|
<%= link_to "Dashboard", root_path, class: theme_link_hover_text_class %>
|
|
8
8
|
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
9
9
|
<%= link_to resource_config.human_name_plural, url_for(action: :index), class: theme_link_hover_text_class %>
|
|
10
10
|
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
11
|
-
<span class="text-slate-900
|
|
11
|
+
<span class="text-slate-900">New</span>
|
|
12
12
|
</nav>
|
|
13
|
-
<h1 class="text-2xl font-bold text-slate-900
|
|
13
|
+
<h1 class="text-2xl font-bold text-slate-900">New <%= resource_config.human_name %></h1>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
16
|
<!-- Form -->
|
|
17
|
-
<div class="bg-white
|
|
17
|
+
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
18
18
|
<div class="p-6">
|
|
19
19
|
<%= render "admin_suite/shared/form", resource: @resource %>
|
|
20
20
|
</div>
|
|
@@ -26,23 +26,23 @@
|
|
|
26
26
|
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
|
|
27
27
|
<!-- Header -->
|
|
28
28
|
<div class="mb-6">
|
|
29
|
-
<nav class="flex items-center gap-2 text-sm text-slate-500
|
|
29
|
+
<nav class="flex items-center gap-2 text-sm text-slate-500 mb-2">
|
|
30
30
|
<%= link_to "Dashboard", root_path, class: theme_link_hover_text_class %>
|
|
31
31
|
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
32
32
|
<%= link_to resource_config.human_name_plural, url_for(action: :index), class: theme_link_hover_text_class %>
|
|
33
33
|
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
34
|
-
<span class="text-slate-900
|
|
34
|
+
<span class="text-slate-900">
|
|
35
35
|
<%= resource.respond_to?(:name) ? resource.name.to_s.truncate(30) : "##{resource.id}" %>
|
|
36
36
|
</span>
|
|
37
37
|
</nav>
|
|
38
38
|
|
|
39
39
|
<div class="flex items-center justify-between">
|
|
40
|
-
<h1 class="text-2xl font-bold text-slate-900
|
|
40
|
+
<h1 class="text-2xl font-bold text-slate-900">
|
|
41
41
|
<%= resource.respond_to?(:name) ? resource.name : "#{resource_config.human_name} ##{resource.id}" %>
|
|
42
42
|
</h1>
|
|
43
43
|
<div class="flex items-center gap-2">
|
|
44
44
|
<% if has_edit_route %>
|
|
45
|
-
<%= link_to url_for(action: :edit, id: resource.to_param), class: "inline-flex items-center gap-2 px-4 py-2 bg-slate-100 hover:bg-slate-200
|
|
45
|
+
<%= link_to url_for(action: :edit, id: resource.to_param), class: "inline-flex items-center gap-2 px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-900 text-sm font-medium rounded-lg transition-colors" do %>
|
|
46
46
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
47
47
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
|
48
48
|
</svg>
|
|
@@ -128,24 +128,24 @@
|
|
|
128
128
|
<% end %>
|
|
129
129
|
|
|
130
130
|
<!-- Two Column Layout -->
|
|
131
|
-
<div class="flex flex-col
|
|
131
|
+
<div class="admin-suite-two-col flex flex-col md:flex-row gap-6">
|
|
132
132
|
<!-- Sidebar (Left Column) -->
|
|
133
133
|
<% if sidebar_sections.any? %>
|
|
134
|
-
<div class="
|
|
134
|
+
<div class="admin-suite-two-col__sidebar admin-suite-two-col__sidebar--details md:w-80 flex-shrink-0 space-y-6">
|
|
135
135
|
<% sidebar_sections.each do |section| %>
|
|
136
136
|
<%= render_show_section(resource, section, :sidebar) %>
|
|
137
137
|
<% end %>
|
|
138
138
|
</div>
|
|
139
139
|
<% else %>
|
|
140
|
-
<div class="
|
|
141
|
-
<div class="bg-white
|
|
142
|
-
<div class="px-4 py-3 border-b border-slate-200
|
|
143
|
-
<h3 class="font-medium text-slate-900
|
|
140
|
+
<div class="admin-suite-two-col__sidebar admin-suite-two-col__sidebar--details md:w-80 flex-shrink-0 space-y-6">
|
|
141
|
+
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
142
|
+
<div class="px-4 py-3 border-b border-slate-200 bg-slate-50">
|
|
143
|
+
<h3 class="font-medium text-slate-900 text-sm">Details</h3>
|
|
144
144
|
</div>
|
|
145
145
|
<div class="p-4 space-y-3">
|
|
146
146
|
<div class="flex justify-between items-start">
|
|
147
|
-
<span class="text-xs font-medium text-slate-500
|
|
148
|
-
<span class="text-sm font-mono text-slate-900
|
|
147
|
+
<span class="text-xs font-medium text-slate-500 uppercase tracking-wider">ID</span>
|
|
148
|
+
<span class="text-sm font-mono text-slate-900"><%= resource.id %></span>
|
|
149
149
|
</div>
|
|
150
150
|
</div>
|
|
151
151
|
</div>
|
|
@@ -159,18 +159,18 @@
|
|
|
159
159
|
<%= render_show_section(resource, section, :main) %>
|
|
160
160
|
<% end %>
|
|
161
161
|
<% else %>
|
|
162
|
-
<div class="bg-white
|
|
163
|
-
<div class="px-6 py-4 border-b border-slate-200
|
|
164
|
-
<h3 class="font-medium text-slate-900
|
|
162
|
+
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
163
|
+
<div class="px-6 py-4 border-b border-slate-200 bg-slate-50">
|
|
164
|
+
<h3 class="font-medium text-slate-900">All Attributes</h3>
|
|
165
165
|
</div>
|
|
166
166
|
<div class="p-6">
|
|
167
167
|
<dl class="space-y-6">
|
|
168
168
|
<% resource.attributes.except("id", "created_at", "updated_at").each do |attr, _value| %>
|
|
169
169
|
<div>
|
|
170
|
-
<dt class="text-sm font-medium text-slate-500
|
|
170
|
+
<dt class="text-sm font-medium text-slate-500 mb-2">
|
|
171
171
|
<%= attr.humanize %>
|
|
172
172
|
</dt>
|
|
173
|
-
<dd class="text-sm text-slate-900
|
|
173
|
+
<dd class="text-sm text-slate-900">
|
|
174
174
|
<%= format_show_value(resource, attr) %>
|
|
175
175
|
</dd>
|
|
176
176
|
</div>
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
<%= form_with model: resource, url: resource.new_record? ? url_for(action: :create) : url_for(action: :update, id: resource.to_param), local: true, class: "space-y-6" do |f| %>
|
|
4
4
|
<% if resource.errors.any? %>
|
|
5
|
-
<div class="p-4 bg-red-50
|
|
5
|
+
<div class="p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
6
6
|
<div class="flex items-start gap-3">
|
|
7
7
|
<svg class="w-5 h-5 text-red-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
8
8
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
9
9
|
</svg>
|
|
10
10
|
<div>
|
|
11
|
-
<h3 class="text-sm font-medium text-red-800
|
|
11
|
+
<h3 class="text-sm font-medium text-red-800">
|
|
12
12
|
<%= pluralize(resource.errors.count, "error") %> prohibited this record from being saved:
|
|
13
13
|
</h3>
|
|
14
|
-
<ul class="mt-2 text-sm text-red-700
|
|
14
|
+
<ul class="mt-2 text-sm text-red-700 list-disc list-inside">
|
|
15
15
|
<% resource.errors.full_messages.each do |message| %>
|
|
16
16
|
<li><%= message %></li>
|
|
17
17
|
<% end %>
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
<% resource_config.form_config&.fields_list&.each do |field| %>
|
|
25
25
|
<% case field %>
|
|
26
26
|
<% when Admin::Base::Resource::SectionDefinition %>
|
|
27
|
-
<div class="border-t border-slate-200
|
|
28
|
-
<h3 class="text-lg font-medium text-slate-900
|
|
27
|
+
<div class="border-t border-slate-200 pt-6 mt-6">
|
|
28
|
+
<h3 class="text-lg font-medium text-slate-900 mb-1"><%= field.title %></h3>
|
|
29
29
|
<% if field.description.present? %>
|
|
30
|
-
<p class="text-sm text-slate-500
|
|
30
|
+
<p class="text-sm text-slate-500 mb-4"><%= field.description %></p>
|
|
31
31
|
<% end %>
|
|
32
32
|
</div>
|
|
33
33
|
<% when Admin::Base::Resource::SectionEnd %>
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
<% end %>
|
|
52
52
|
<% end %>
|
|
53
53
|
|
|
54
|
-
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200
|
|
55
|
-
<%= link_to "Cancel", resource.new_record? ? url_for(action: :index) : url_for(action: :show, id: resource.to_param), class: "px-4 py-2 text-sm font-medium text-slate-700
|
|
54
|
+
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200">
|
|
55
|
+
<%= link_to "Cancel", resource.new_record? ? url_for(action: :index) : url_for(action: :show, id: resource.to_param), class: "px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 rounded-lg transition-colors" %>
|
|
56
56
|
<%= f.button class: theme_btn_primary_small_class, data: { disable_with: "Saving..." } do %>
|
|
57
57
|
<%= resource.new_record? ? "Create #{resource_config.human_name}" : "Save Changes" %>
|
|
58
58
|
<% end %>
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
<div data-controller="admin-suite--json-editor">
|
|
20
20
|
<% if field.help.present? %>
|
|
21
|
-
<p class="text-sm text-slate-500
|
|
21
|
+
<p class="text-sm text-slate-500 mb-2"><%= field.help %></p>
|
|
22
22
|
<% else %>
|
|
23
|
-
<p class="text-sm text-slate-500
|
|
23
|
+
<p class="text-sm text-slate-500 mb-2">Enter valid JSON. Use the Format button to pretty-print.</p>
|
|
24
24
|
<% end %>
|
|
25
25
|
|
|
26
26
|
<div class="relative">
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
<button
|
|
39
39
|
type="button"
|
|
40
40
|
data-action="click->admin-suite--json-editor#format"
|
|
41
|
-
class="px-3 py-1.5 text-xs font-medium text-slate-700
|
|
41
|
+
class="px-3 py-1.5 text-xs font-medium text-slate-700 bg-slate-100 hover:bg-slate-200 rounded transition-colors"
|
|
42
42
|
>
|
|
43
43
|
Format JSON
|
|
44
44
|
</button>
|
|
@@ -47,6 +47,6 @@
|
|
|
47
47
|
|
|
48
48
|
<div
|
|
49
49
|
data-admin-suite--json-editor-target="error"
|
|
50
|
-
class="hidden mt-2 text-sm text-red-600
|
|
50
|
+
class="hidden mt-2 text-sm text-red-600"
|
|
51
51
|
></div>
|
|
52
52
|
</div>
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
<!-- Portal Navigation -->
|
|
45
45
|
<nav class="flex-1 overflow-y-auto px-3 py-4 space-y-2">
|
|
46
46
|
<% sorted_portals.each do |portal_key, portal| %>
|
|
47
|
-
<% portal_home = portal_path(portal: portal_key) %>
|
|
48
|
-
<% portal_prefix =
|
|
49
|
-
<% active = request.path.start_with?(portal_prefix) %>
|
|
47
|
+
<% portal_home = portal_path(portal: portal_key).to_s %>
|
|
48
|
+
<% portal_prefix = portal_home.sub(%r{/*\z}, "") %>
|
|
49
|
+
<% active = portal_prefix.present? && (request.path == portal_prefix || request.path.start_with?("#{portal_prefix}/")) %>
|
|
50
50
|
<% color = portal_color(portal_key) %>
|
|
51
51
|
|
|
52
52
|
<div>
|
|
53
|
-
<%= link_to (
|
|
53
|
+
<%= link_to (portal_prefix.presence || root_path),
|
|
54
54
|
class: "flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors #{active ? "bg-white/20 text-white" : "text-white/90 hover:bg-white/10 hover:text-white"}" do %>
|
|
55
55
|
<div class="admin-suite-portal-accent admin-suite-portal-accent--<%= color %> admin-suite-portal-chip flex items-center justify-center w-8 h-8 rounded-lg <%= active ? "" : "bg-white/10" %>">
|
|
56
56
|
<%= portal_icon(portal_key, class: "admin-suite-portal-icon w-4 h-4") %>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<div class="flex items-center gap-4">
|
|
7
7
|
<!-- Mobile menu button -->
|
|
8
8
|
<button type="button"
|
|
9
|
-
class="lg:hidden p-2 rounded-lg text-slate-500 hover:bg-slate-100"
|
|
9
|
+
class="admin-suite-mobile-menu-button lg:hidden p-2 rounded-lg text-slate-500 hover:bg-slate-100"
|
|
10
10
|
data-action="click->admin-suite--sidebar#toggle">
|
|
11
11
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
12
12
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
|
@@ -41,24 +41,24 @@
|
|
|
41
41
|
<body class="admin-suite bg-slate-50">
|
|
42
42
|
<div class="flex h-screen overflow-hidden" data-controller="admin-suite--sidebar">
|
|
43
43
|
<!-- Sidebar -->
|
|
44
|
-
<div class="
|
|
44
|
+
<div class="admin-suite-desktop-sidebar lg:flex lg:flex-shrink-0">
|
|
45
45
|
<%= render "admin_suite/shared/sidebar" %>
|
|
46
46
|
</div>
|
|
47
47
|
|
|
48
48
|
<!-- Mobile sidebar overlay -->
|
|
49
49
|
<div data-admin-suite--sidebar-target="overlay"
|
|
50
|
-
class="hidden fixed inset-0 z-40 bg-slate-600 bg-opacity-75 lg:hidden"
|
|
50
|
+
class="admin-suite-mobile-overlay hidden fixed inset-0 z-40 bg-slate-600 bg-opacity-75 lg:hidden"
|
|
51
51
|
data-action="click->admin-suite--sidebar#close">
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
54
|
<!-- Mobile sidebar -->
|
|
55
55
|
<div data-admin-suite--sidebar-target="mobileSidebar"
|
|
56
|
-
class="hidden fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out lg:hidden">
|
|
56
|
+
class="admin-suite-mobile-sidebar hidden fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out lg:hidden">
|
|
57
57
|
<%= render "admin_suite/shared/sidebar" %>
|
|
58
58
|
</div>
|
|
59
59
|
|
|
60
60
|
<!-- Main content area -->
|
|
61
|
-
<div class="flex flex-col flex-1 overflow-hidden">
|
|
61
|
+
<div class="admin-suite-main flex flex-col flex-1 overflow-hidden">
|
|
62
62
|
<!-- Top bar -->
|
|
63
63
|
<%= render "admin_suite/shared/topbar" %>
|
|
64
64
|
|
data/docs/configuration.md
CHANGED
|
@@ -32,16 +32,32 @@ 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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`app/
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
- `dashboard_globs`: defaults to:
|
|
43
|
+
- `Rails.root/config/admin_suite/dashboard.rb`
|
|
44
|
+
- `Rails.root/config/admin_suite/dashboard/*.rb`
|
|
45
|
+
- `Rails.root/app/admin_suite/dashboard.rb`
|
|
46
|
+
- `Rails.root/app/admin_suite/dashboard/*.rb`
|
|
47
|
+
|
|
48
|
+
Note: AdminSuite definition files (resources, actions, portals) often don't follow
|
|
49
|
+
Zeitwerk's path-to-constant naming conventions. To prevent eager-load `Zeitwerk::NameError`s
|
|
50
|
+
in production, the engine only configures Zeitwerk to ignore these directories and load them via globs instead:
|
|
51
|
+
- `app/admin_suite`
|
|
52
|
+
- `app/admin/portals` (when portal DSL usage is detected)
|
|
53
|
+
|
|
54
|
+
Other `app/admin/*` directories (such as `app/admin/resources`, `app/admin/actions`, and `app/admin/base`) are
|
|
55
|
+
not ignored by default and may be treated as normal Zeitwerk autoload paths if they are added to the loader
|
|
56
|
+
(for example, via `loader.push_dir("app/admin")` in the host app). Do not rely on these directories being
|
|
57
|
+
ignored for autoloading; instead, keep files there Zeitwerk-compatible.
|
|
58
|
+
|
|
59
|
+
We recommend placing non-Zeitwerk-compatible definition files under `config/admin_suite/*` or `app/admin_suite/*`
|
|
60
|
+
for clearer separation from standard Rails autoloading.
|
|
45
61
|
- `portals`: default portal metadata for `:ops`, `:email`, `:ai`, `:assistant`
|
|
46
62
|
- `custom_renderers`: `{}`
|
|
47
63
|
- `icon_renderer`: `nil` (uses lucide-rails by default when available)
|
|
@@ -95,12 +111,46 @@ config.resource_globs = [
|
|
|
95
111
|
]
|
|
96
112
|
```
|
|
97
113
|
|
|
114
|
+
### `action_globs`
|
|
115
|
+
|
|
116
|
+
Where AdminSuite should load action handler files from (files that define custom action handlers, typically subclasses of `Admin::Base::ActionHandler`).
|
|
117
|
+
|
|
118
|
+
- **Type**: `Array<String>`
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
config.action_globs = [
|
|
124
|
+
Rails.root.join("app/admin/actions/**/*.rb").to_s
|
|
125
|
+
]
|
|
126
|
+
```
|
|
127
|
+
|
|
98
128
|
### `portal_globs`
|
|
99
129
|
|
|
100
130
|
Where AdminSuite should load portal definition files from (files typically call `AdminSuite.portal(:key) { ... }`).
|
|
101
131
|
|
|
102
132
|
- **Type**: `Array<String>`
|
|
103
133
|
|
|
134
|
+
### `dashboard_globs`
|
|
135
|
+
|
|
136
|
+
Where AdminSuite should load the root dashboard definition file(s) from (files typically call `AdminSuite.root_dashboard { ... }`).
|
|
137
|
+
|
|
138
|
+
- **Type**: `Array<String>`
|
|
139
|
+
|
|
140
|
+
### `root_dashboard_title`
|
|
141
|
+
|
|
142
|
+
Optional title shown on the root dashboard.
|
|
143
|
+
|
|
144
|
+
- **Type**: `String`, `Proc`, or `nil`
|
|
145
|
+
- **Proc signature**: `->(controller) { "Admin Suite" }`
|
|
146
|
+
|
|
147
|
+
### `root_dashboard_description`
|
|
148
|
+
|
|
149
|
+
Optional description shown on the root dashboard.
|
|
150
|
+
|
|
151
|
+
- **Type**: `String`, `Proc`, or `nil`
|
|
152
|
+
- **Proc signature**: `->(controller) { "..." }`
|
|
153
|
+
|
|
104
154
|
### `portals`
|
|
105
155
|
|
|
106
156
|
Portal metadata used for navigation (label/icon/color/order). This is separate from the portal DSL and can be used alone.
|
data/docs/portals.md
CHANGED
|
@@ -96,3 +96,45 @@ Example:
|
|
|
96
96
|
- `/internal/admin/ops`
|
|
97
97
|
- `/internal/admin/ai`
|
|
98
98
|
|
|
99
|
+
## Root dashboard (`AdminSuite.root_dashboard`)
|
|
100
|
+
|
|
101
|
+
The engine root (`/`, relative to the mount path) renders a default dashboard (portal cards + basic stats).
|
|
102
|
+
|
|
103
|
+
To customize it, create a dashboard definition file (loaded from `AdminSuite.config.dashboard_globs`, which defaults to paths like `config/admin_suite/dashboard.rb` and `app/admin_suite/dashboard.rb`):
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# config/admin_suite/dashboard.rb
|
|
107
|
+
AdminSuite.configure do |config|
|
|
108
|
+
config.root_dashboard_title = "Developer Portal"
|
|
109
|
+
config.root_dashboard_description = "Internal tools for managing application resources."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
AdminSuite.root_dashboard do
|
|
113
|
+
row do
|
|
114
|
+
cards_panel "Portals",
|
|
115
|
+
span: 12,
|
|
116
|
+
variant: :portals,
|
|
117
|
+
resources: ->(view) do
|
|
118
|
+
view.navigation_items
|
|
119
|
+
.sort_by { |(_k, meta)| (meta[:order] || 100).to_i }
|
|
120
|
+
.map do |portal_key, portal|
|
|
121
|
+
{
|
|
122
|
+
key: portal_key,
|
|
123
|
+
label: portal[:label] || portal_key.to_s.humanize,
|
|
124
|
+
description: portal[:description],
|
|
125
|
+
color: view.portal_color(portal_key),
|
|
126
|
+
icon: portal[:icon],
|
|
127
|
+
path: view.portal_path(portal: portal_key),
|
|
128
|
+
count: (portal[:sections] || {}).values.sum { |s| Array(s[:items]).size }
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
row do
|
|
135
|
+
stat_panel "Portals", ->(view) { view.navigation_items.keys.count }, span: 6, variant: :mini, color: :slate
|
|
136
|
+
stat_panel "Resources", -> { Admin::Base::Resource.registered_resources.count }, span: 6, variant: :mini, color: :slate
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
@@ -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,7 +7,9 @@ module AdminSuite
|
|
|
7
7
|
:current_actor,
|
|
8
8
|
:authorize,
|
|
9
9
|
:resource_globs,
|
|
10
|
+
:action_globs,
|
|
10
11
|
:portal_globs,
|
|
12
|
+
:dashboard_globs,
|
|
11
13
|
:portals,
|
|
12
14
|
:custom_renderers,
|
|
13
15
|
:icon_renderer,
|
|
@@ -17,6 +19,10 @@ module AdminSuite
|
|
|
17
19
|
:theme,
|
|
18
20
|
:host_stylesheet,
|
|
19
21
|
:tailwind_cdn,
|
|
22
|
+
:root_dashboard_title,
|
|
23
|
+
:root_dashboard_description,
|
|
24
|
+
:root_dashboard_definition,
|
|
25
|
+
:root_dashboard_loaded,
|
|
20
26
|
:on_action_executed,
|
|
21
27
|
:resolve_action_handler
|
|
22
28
|
|
|
@@ -25,7 +31,9 @@ module AdminSuite
|
|
|
25
31
|
@current_actor = nil
|
|
26
32
|
@authorize = nil
|
|
27
33
|
@resource_globs = []
|
|
34
|
+
@action_globs = []
|
|
28
35
|
@portal_globs = []
|
|
36
|
+
@dashboard_globs = []
|
|
29
37
|
@portals = {}
|
|
30
38
|
@custom_renderers = {}
|
|
31
39
|
@icon_renderer = nil
|
|
@@ -35,6 +43,10 @@ module AdminSuite
|
|
|
35
43
|
@theme = { primary: :indigo, secondary: :purple }
|
|
36
44
|
@host_stylesheet = nil
|
|
37
45
|
@tailwind_cdn = true
|
|
46
|
+
@root_dashboard_title = nil
|
|
47
|
+
@root_dashboard_description = nil
|
|
48
|
+
@root_dashboard_definition = nil
|
|
49
|
+
@root_dashboard_loaded = false
|
|
38
50
|
@on_action_executed = nil
|
|
39
51
|
@resolve_action_handler = nil
|
|
40
52
|
end
|