admin_suite 0.2.1 → 0.2.3
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 +129 -0
- data/app/controllers/admin_suite/application_controller.rb +31 -0
- data/app/controllers/admin_suite/dashboard_controller.rb +59 -226
- data/app/controllers/admin_suite/resources_controller.rb +14 -4
- data/app/helpers/admin_suite/base_helper.rb +147 -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/flash_controller.js +45 -0
- 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 +25 -16
- 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/_flash.html.erb +15 -2
- 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/_toggle_cell.html.erb +4 -2
- data/app/views/admin_suite/shared/_topbar.html.erb +14 -1
- data/app/views/layouts/admin_suite/application.html.erb +4 -4
- data/docs/configuration.md +55 -0
- data/docs/portals.md +42 -0
- data/lib/admin_suite/configuration.rb +16 -0
- data/lib/admin_suite/engine.rb +9 -0
- 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 +31 -3
- 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 +5 -0
- data/test/integration/dashboard_test.rb +57 -1
- metadata +7 -5
- data/test/dummy/log/test.log +0 -624
- data/test/dummy/tmp/local_secret.txt +0 -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 dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-
|
|
17
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-visible">
|
|
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>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<% if flash.any? %>
|
|
2
|
-
<div class="fixed top-
|
|
2
|
+
<div class="fixed top-20 right-4 z-50 space-y-2 max-w-md" data-controller="admin-suite--flash">
|
|
3
3
|
<% flash.each do |type, message| %>
|
|
4
4
|
<% type = type.to_sym %>
|
|
5
5
|
<% classes = case type
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
"admin-suite-flash--info"
|
|
14
14
|
end %>
|
|
15
15
|
|
|
16
|
-
<div
|
|
16
|
+
<div
|
|
17
|
+
data-admin-suite--flash-target="notification"
|
|
18
|
+
class="flex items-start gap-3 px-5 py-4 rounded-lg shadow-lg border <%= classes %>"
|
|
19
|
+
>
|
|
17
20
|
<div class="flex-shrink-0 mt-0.5">
|
|
18
21
|
<% icon_name = case type
|
|
19
22
|
when :notice then "check-circle-2"
|
|
@@ -23,7 +26,17 @@
|
|
|
23
26
|
end %>
|
|
24
27
|
<%= admin_suite_icon(icon_name, class: "w-5 h-5") %>
|
|
25
28
|
</div>
|
|
29
|
+
|
|
26
30
|
<p class="flex-1 text-sm font-medium leading-snug"><%= message %></p>
|
|
31
|
+
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
class="flex-shrink-0 text-slate-400 hover:text-slate-600 transition-colors"
|
|
35
|
+
data-action="admin-suite--flash#dismiss"
|
|
36
|
+
aria-label="Dismiss notification"
|
|
37
|
+
>
|
|
38
|
+
<%= admin_suite_icon("x", class: "w-4 h-4") %>
|
|
39
|
+
</button>
|
|
27
40
|
</div>
|
|
28
41
|
<% end %>
|
|
29
42
|
</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") %>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
%>
|
|
8
8
|
<%
|
|
9
9
|
value = record.public_send(field)
|
|
10
|
+
frame_id = dom_id(record, "#{field}_toggle")
|
|
10
11
|
toggle_url ||= begin
|
|
11
12
|
url_for(action: :toggle, id: record.id, field: field)
|
|
12
13
|
rescue ActionController::UrlGenerationError
|
|
@@ -16,11 +17,12 @@
|
|
|
16
17
|
end
|
|
17
18
|
%>
|
|
18
19
|
|
|
19
|
-
<%= turbo_frame_tag
|
|
20
|
+
<%= turbo_frame_tag frame_id, class: "admin-suite-toggle-wrap inline-flex align-middle" do %>
|
|
20
21
|
<% if toggle_url %>
|
|
21
22
|
<%= button_to toggle_url,
|
|
22
23
|
method: :post,
|
|
23
|
-
|
|
24
|
+
params: { field: field },
|
|
25
|
+
form: { data: { turbo_frame: frame_id }, class: "m-0 inline-flex align-middle items-center" },
|
|
24
26
|
class: "admin-suite-toggle-track #{value ? 'is-on' : ''}" do %>
|
|
25
27
|
<span class="sr-only">Toggle <%= field.to_s.humanize %></span>
|
|
26
28
|
<span class="admin-suite-toggle-thumb"></span>
|
|
@@ -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"/>
|
|
@@ -42,6 +42,19 @@
|
|
|
42
42
|
<%= (admin_suite_actor&.respond_to?(:name) && admin_suite_actor.name.present?) ? admin_suite_actor.name.first.upcase : "A" %>
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
|
+
|
|
46
|
+
<% logout_path = admin_suite_logout_path %>
|
|
47
|
+
<% if logout_path.present? %>
|
|
48
|
+
<div class="pl-3 border-l border-slate-200">
|
|
49
|
+
<%= button_to logout_path,
|
|
50
|
+
method: admin_suite_logout_method,
|
|
51
|
+
form: { class: "inline-flex" },
|
|
52
|
+
class: "inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-slate-600 hover:text-red-600 hover:bg-red-50 transition-colors" do %>
|
|
53
|
+
<%= admin_suite_icon("log-out", class: "w-4 h-4") %>
|
|
54
|
+
<span class="hidden sm:inline"><%= admin_suite_logout_label %></span>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
<% end %>
|
|
45
58
|
</div>
|
|
46
59
|
</div>
|
|
47
60
|
</header>
|
|
@@ -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
|
@@ -29,6 +29,9 @@ These are the defaults in `AdminSuite::Configuration` / `AdminSuite::Engine`:
|
|
|
29
29
|
- `authenticate`: `nil`
|
|
30
30
|
- `current_actor`: `nil`
|
|
31
31
|
- `authorize`: `nil`
|
|
32
|
+
- `logout_path`: `nil`
|
|
33
|
+
- `logout_method`: `:delete`
|
|
34
|
+
- `logout_label`: `"Log out"`
|
|
32
35
|
- `resource_globs`: defaults to:
|
|
33
36
|
- `Rails.root/config/admin_suite/resources/*.rb`
|
|
34
37
|
- `Rails.root/app/admin/resources/*.rb`
|
|
@@ -39,6 +42,11 @@ These are the defaults in `AdminSuite::Configuration` / `AdminSuite::Engine`:
|
|
|
39
42
|
- `Rails.root/config/admin_suite/portals/*.rb`
|
|
40
43
|
- `Rails.root/app/admin/portals/*.rb`
|
|
41
44
|
- `Rails.root/app/admin_suite/portals/*.rb`
|
|
45
|
+
- `dashboard_globs`: defaults to:
|
|
46
|
+
- `Rails.root/config/admin_suite/dashboard.rb`
|
|
47
|
+
- `Rails.root/config/admin_suite/dashboard/*.rb`
|
|
48
|
+
- `Rails.root/app/admin_suite/dashboard.rb`
|
|
49
|
+
- `Rails.root/app/admin_suite/dashboard/*.rb`
|
|
42
50
|
|
|
43
51
|
Note: AdminSuite definition files (resources, actions, portals) often don't follow
|
|
44
52
|
Zeitwerk's path-to-constant naming conventions. To prevent eager-load `Zeitwerk::NameError`s
|
|
@@ -92,6 +100,33 @@ Optional authorization hook (you can wire Pundit/CanCan/ActionPolicy/etc).
|
|
|
92
100
|
|
|
93
101
|
Note: this hook is available, but your app must call it from resource definitions / custom actions as needed (AdminSuite will not guess your authorization policy).
|
|
94
102
|
|
|
103
|
+
### `logout_path`
|
|
104
|
+
|
|
105
|
+
Optional sign-out action shown in the top bar.
|
|
106
|
+
|
|
107
|
+
- **Type**: `Proc`, `String`, `Symbol`, or `nil`
|
|
108
|
+
- **Proc signature**: `->(view_context) { ... }`
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
config.logout_path = ->(view) { view.main_app.internal_developer_logout_path }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `logout_method`
|
|
117
|
+
|
|
118
|
+
HTTP method for the topbar sign-out button.
|
|
119
|
+
|
|
120
|
+
- **Type**: `Symbol` or `String`
|
|
121
|
+
- **Default**: `:delete`
|
|
122
|
+
|
|
123
|
+
### `logout_label`
|
|
124
|
+
|
|
125
|
+
Button label for the topbar sign-out action.
|
|
126
|
+
|
|
127
|
+
- **Type**: `String` (or `Proc` for dynamic label)
|
|
128
|
+
- **Default**: `"Log out"`
|
|
129
|
+
|
|
95
130
|
### `resource_globs`
|
|
96
131
|
|
|
97
132
|
Where AdminSuite should load resource definition files from.
|
|
@@ -126,6 +161,26 @@ Where AdminSuite should load portal definition files from (files typically call
|
|
|
126
161
|
|
|
127
162
|
- **Type**: `Array<String>`
|
|
128
163
|
|
|
164
|
+
### `dashboard_globs`
|
|
165
|
+
|
|
166
|
+
Where AdminSuite should load the root dashboard definition file(s) from (files typically call `AdminSuite.root_dashboard { ... }`).
|
|
167
|
+
|
|
168
|
+
- **Type**: `Array<String>`
|
|
169
|
+
|
|
170
|
+
### `root_dashboard_title`
|
|
171
|
+
|
|
172
|
+
Optional title shown on the root dashboard.
|
|
173
|
+
|
|
174
|
+
- **Type**: `String`, `Proc`, or `nil`
|
|
175
|
+
- **Proc signature**: `->(controller) { "Admin Suite" }`
|
|
176
|
+
|
|
177
|
+
### `root_dashboard_description`
|
|
178
|
+
|
|
179
|
+
Optional description shown on the root dashboard.
|
|
180
|
+
|
|
181
|
+
- **Type**: `String`, `Proc`, or `nil`
|
|
182
|
+
- **Proc signature**: `->(controller) { "..." }`
|
|
183
|
+
|
|
129
184
|
### `portals`
|
|
130
185
|
|
|
131
186
|
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
|
+
|
|
@@ -6,9 +6,13 @@ module AdminSuite
|
|
|
6
6
|
attr_accessor :authenticate,
|
|
7
7
|
:current_actor,
|
|
8
8
|
:authorize,
|
|
9
|
+
:logout_path,
|
|
10
|
+
:logout_method,
|
|
11
|
+
:logout_label,
|
|
9
12
|
:resource_globs,
|
|
10
13
|
:action_globs,
|
|
11
14
|
:portal_globs,
|
|
15
|
+
:dashboard_globs,
|
|
12
16
|
:portals,
|
|
13
17
|
:custom_renderers,
|
|
14
18
|
:icon_renderer,
|
|
@@ -18,6 +22,10 @@ module AdminSuite
|
|
|
18
22
|
:theme,
|
|
19
23
|
:host_stylesheet,
|
|
20
24
|
:tailwind_cdn,
|
|
25
|
+
:root_dashboard_title,
|
|
26
|
+
:root_dashboard_description,
|
|
27
|
+
:root_dashboard_definition,
|
|
28
|
+
:root_dashboard_loaded,
|
|
21
29
|
:on_action_executed,
|
|
22
30
|
:resolve_action_handler
|
|
23
31
|
|
|
@@ -25,9 +33,13 @@ module AdminSuite
|
|
|
25
33
|
@authenticate = nil
|
|
26
34
|
@current_actor = nil
|
|
27
35
|
@authorize = nil
|
|
36
|
+
@logout_path = nil
|
|
37
|
+
@logout_method = :delete
|
|
38
|
+
@logout_label = "Log out"
|
|
28
39
|
@resource_globs = []
|
|
29
40
|
@action_globs = []
|
|
30
41
|
@portal_globs = []
|
|
42
|
+
@dashboard_globs = []
|
|
31
43
|
@portals = {}
|
|
32
44
|
@custom_renderers = {}
|
|
33
45
|
@icon_renderer = nil
|
|
@@ -37,6 +49,10 @@ module AdminSuite
|
|
|
37
49
|
@theme = { primary: :indigo, secondary: :purple }
|
|
38
50
|
@host_stylesheet = nil
|
|
39
51
|
@tailwind_cdn = true
|
|
52
|
+
@root_dashboard_title = nil
|
|
53
|
+
@root_dashboard_description = nil
|
|
54
|
+
@root_dashboard_definition = nil
|
|
55
|
+
@root_dashboard_loaded = false
|
|
40
56
|
@on_action_executed = nil
|
|
41
57
|
@resolve_action_handler = nil
|
|
42
58
|
end
|