admin_suite 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +7 -0
- data/Rakefile +11 -0
- data/app/assets/admin_suite.css +444 -0
- data/app/assets/admin_suite_tailwind.css +8 -0
- data/app/assets/builds/admin_suite_tailwind.css +8 -0
- data/app/assets/rouge.css +218 -0
- data/app/assets/tailwind/admin_suite.css +22 -0
- data/app/controllers/admin_suite/application_controller.rb +118 -0
- data/app/controllers/admin_suite/dashboard_controller.rb +258 -0
- data/app/controllers/admin_suite/docs_controller.rb +155 -0
- data/app/controllers/admin_suite/portals_controller.rb +22 -0
- data/app/controllers/admin_suite/resources_controller.rb +238 -0
- data/app/helpers/admin_suite/base_helper.rb +1199 -0
- data/app/helpers/admin_suite/icon_helper.rb +61 -0
- data/app/helpers/admin_suite/panels_helper.rb +52 -0
- data/app/helpers/admin_suite/resources_helper.rb +15 -0
- data/app/helpers/admin_suite/theme_helper.rb +99 -0
- data/app/javascript/controllers/admin_suite/click_actions_controller.js +73 -0
- data/app/javascript/controllers/admin_suite/clipboard_controller.js +57 -0
- data/app/javascript/controllers/admin_suite/code_editor_controller.js +45 -0
- data/app/javascript/controllers/admin_suite/file_upload_controller.js +238 -0
- data/app/javascript/controllers/admin_suite/json_editor_controller.js +62 -0
- data/app/javascript/controllers/admin_suite/live_filter_controller.js +71 -0
- data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +67 -0
- data/app/javascript/controllers/admin_suite/searchable_select_controller.js +171 -0
- data/app/javascript/controllers/admin_suite/sidebar_controller.js +33 -0
- data/app/javascript/controllers/admin_suite/tag_select_controller.js +193 -0
- data/app/javascript/controllers/admin_suite/toggle_switch_controller.js +66 -0
- data/app/views/admin_suite/dashboard/index.html.erb +21 -0
- data/app/views/admin_suite/docs/index.html.erb +86 -0
- data/app/views/admin_suite/panels/_cards.html.erb +107 -0
- data/app/views/admin_suite/panels/_chart.html.erb +47 -0
- data/app/views/admin_suite/panels/_health.html.erb +44 -0
- data/app/views/admin_suite/panels/_recent.html.erb +56 -0
- data/app/views/admin_suite/panels/_stat.html.erb +64 -0
- data/app/views/admin_suite/panels/_table.html.erb +36 -0
- data/app/views/admin_suite/portals/show.html.erb +75 -0
- data/app/views/admin_suite/resources/_form.html.erb +32 -0
- data/app/views/admin_suite/resources/edit.html.erb +24 -0
- data/app/views/admin_suite/resources/index.html.erb +315 -0
- data/app/views/admin_suite/resources/new.html.erb +22 -0
- data/app/views/admin_suite/resources/show.html.erb +184 -0
- data/app/views/admin_suite/shared/_flash.html.erb +30 -0
- data/app/views/admin_suite/shared/_form.html.erb +60 -0
- data/app/views/admin_suite/shared/_json_editor_field.html.erb +52 -0
- data/app/views/admin_suite/shared/_sidebar.html.erb +94 -0
- data/app/views/admin_suite/shared/_toggle_cell.html.erb +34 -0
- data/app/views/admin_suite/shared/_topbar.html.erb +47 -0
- data/app/views/layouts/admin_suite/application.html.erb +79 -0
- data/lib/admin/base/action_executor.rb +155 -0
- data/lib/admin/base/action_handler.rb +31 -0
- data/lib/admin/base/filter_builder.rb +121 -0
- data/lib/admin/base/resource.rb +541 -0
- data/lib/admin_suite/configuration.rb +42 -0
- data/lib/admin_suite/engine.rb +101 -0
- data/lib/admin_suite/markdown_renderer.rb +115 -0
- data/lib/admin_suite/portal_definition.rb +64 -0
- data/lib/admin_suite/portal_registry.rb +32 -0
- data/lib/admin_suite/theme_palette.rb +36 -0
- data/lib/admin_suite/ui/dashboard_definition.rb +69 -0
- data/lib/admin_suite/ui/field_renderer_registry.rb +119 -0
- data/lib/admin_suite/ui/form_field_renderer.rb +48 -0
- data/lib/admin_suite/ui/show_formatter_registry.rb +120 -0
- data/lib/admin_suite/ui/show_value_formatter.rb +70 -0
- data/lib/admin_suite/version.rb +10 -0
- data/lib/admin_suite.rb +54 -0
- data/lib/generators/admin_suite/install/install_generator.rb +23 -0
- data/lib/generators/admin_suite/install/templates/admin_suite.rb +60 -0
- data/lib/generators/admin_suite/resource/resource_generator.rb +83 -0
- data/lib/generators/admin_suite/resource/templates/resource.rb.tt +47 -0
- data/lib/generators/admin_suite/scaffold/scaffold_generator.rb +28 -0
- data/lib/tasks/admin_suite_tailwind.rake +28 -0
- data/lib/tasks/admin_suite_test.rake +11 -0
- data/test/dummy/Gemfile +21 -0
- data/test/dummy/README.md +24 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/stylesheets/application.css +10 -0
- data/test/dummy/app/controllers/application_controller.rb +4 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/application_record.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +28 -0
- data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
- data/test/dummy/app/views/pwa/service-worker.js +26 -0
- data/test/dummy/bin/ci +6 -0
- data/test/dummy/bin/dev +2 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +35 -0
- data/test/dummy/config/application.rb +43 -0
- data/test/dummy/config/boot.rb +3 -0
- data/test/dummy/config/ci.rb +19 -0
- data/test/dummy/config/database.yml +31 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +57 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +7 -0
- data/test/dummy/config/initializers/content_security_policy.rb +29 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +8 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/locales/en.yml +31 -0
- data/test/dummy/config/puma.rb +39 -0
- data/test/dummy/config/routes.rb +16 -0
- data/test/dummy/config.ru +6 -0
- data/test/dummy/db/seeds.rb +9 -0
- data/test/dummy/log/test.log +441 -0
- data/test/dummy/public/400.html +135 -0
- data/test/dummy/public/404.html +135 -0
- data/test/dummy/public/406-unsupported-browser.html +135 -0
- data/test/dummy/public/422.html +135 -0
- data/test/dummy/public/500.html +135 -0
- data/test/dummy/public/icon.png +0 -0
- data/test/dummy/public/icon.svg +3 -0
- data/test/dummy/public/robots.txt +1 -0
- data/test/dummy/test/test_helper.rb +15 -0
- data/test/dummy/tmp/local_secret.txt +1 -0
- data/test/fixtures/docs/progress/PROGRESS_REPORT.md +6 -0
- data/test/integration/dashboard_test.rb +13 -0
- data/test/integration/docs_test.rb +46 -0
- data/test/integration/theme_test.rb +27 -0
- data/test/lib/markdown_renderer_test.rb +20 -0
- data/test/lib/theme_palette_test.rb +24 -0
- data/test/test_helper.rb +11 -0
- metadata +264 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<% content_for :title, "Docs - Admin Suite" %>
|
|
2
|
+
|
|
3
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
4
|
+
<div class="flex items-start gap-6">
|
|
5
|
+
<!-- Sidebar -->
|
|
6
|
+
<aside class="hidden lg:block w-72 flex-shrink-0 sticky top-6 max-h-[calc(100vh-8rem)] overflow-hidden">
|
|
7
|
+
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden h-full">
|
|
8
|
+
<div class="p-4 border-b border-slate-200 bg-slate-50">
|
|
9
|
+
<div class="text-sm font-semibold text-slate-900">Documentation</div>
|
|
10
|
+
<div class="text-xs text-slate-500 mt-1"><%= @docs_root.to_s %></div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<nav class="p-3 space-y-4 overflow-y-auto max-h-[calc(100vh-14rem)]">
|
|
14
|
+
<% if @files.present? %>
|
|
15
|
+
<% @files.each do |group_name, files| %>
|
|
16
|
+
<div>
|
|
17
|
+
<div class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 px-2"><%= group_name %></div>
|
|
18
|
+
<ul class="space-y-0.5">
|
|
19
|
+
<% Array(files).each do |file_path| %>
|
|
20
|
+
<% is_selected = file_path == @selected_path %>
|
|
21
|
+
<li>
|
|
22
|
+
<%= link_to doc_path(file_path),
|
|
23
|
+
class: "flex items-center gap-2 px-2 py-1.5 text-sm rounded-lg transition-colors #{is_selected ? "bg-slate-100 text-slate-900 font-semibold" : "text-slate-600 hover:bg-slate-50 hover:text-slate-900"}" do %>
|
|
24
|
+
<span class="truncate"><%= File.basename(file_path, ".md").tr("_", " ").tr("-", " ").titleize %></span>
|
|
25
|
+
<% end %>
|
|
26
|
+
</li>
|
|
27
|
+
<% end %>
|
|
28
|
+
</ul>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
31
|
+
<% else %>
|
|
32
|
+
<div class="p-4 text-sm text-slate-500">
|
|
33
|
+
No markdown files found.
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
</nav>
|
|
37
|
+
</div>
|
|
38
|
+
</aside>
|
|
39
|
+
|
|
40
|
+
<!-- Main -->
|
|
41
|
+
<section class="flex-1 min-w-0">
|
|
42
|
+
<% if @content_html.present? %>
|
|
43
|
+
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
44
|
+
<div class="px-6 py-5 border-b border-slate-200 bg-slate-50">
|
|
45
|
+
<h1 class="text-2xl font-bold text-slate-900 mb-2"><%= @title %></h1>
|
|
46
|
+
<div class="flex items-center gap-4 text-sm text-slate-500">
|
|
47
|
+
<% if @reading_time %>
|
|
48
|
+
<span><%= @reading_time %> min read</span>
|
|
49
|
+
<% end %>
|
|
50
|
+
<span class="font-mono text-xs"><%= @selected_path %></span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="p-6">
|
|
55
|
+
<% if @toc.present? && @toc.any? %>
|
|
56
|
+
<div class="mb-8 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
|
57
|
+
<div class="text-sm font-semibold text-slate-900 mb-3">Table of contents</div>
|
|
58
|
+
<ul class="space-y-1 text-sm">
|
|
59
|
+
<% @toc.each do |item| %>
|
|
60
|
+
<li style="padding-left: <%= (item[:level] - 2) * 0.75 %>rem">
|
|
61
|
+
<a href="#<%= item[:id] %>" class="admin-suite-link admin-suite-link-hover hover:underline">
|
|
62
|
+
<%= item[:text] %>
|
|
63
|
+
</a>
|
|
64
|
+
</li>
|
|
65
|
+
<% end %>
|
|
66
|
+
</ul>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
70
|
+
<article class="admin-suite-doc-content">
|
|
71
|
+
<%= @content_html.html_safe %>
|
|
72
|
+
</article>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<% else %>
|
|
76
|
+
<div class="bg-white rounded-xl border border-slate-200 p-12 text-center">
|
|
77
|
+
<div class="text-lg font-semibold text-slate-900">No document selected</div>
|
|
78
|
+
<div class="text-sm text-slate-500 mt-1">
|
|
79
|
+
Add markdown files under <code><%= Rails.root.join("docs") %></code> (or set <code>config.docs_path</code>).
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<% end %>
|
|
83
|
+
</section>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<%# trailing newline %>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<% resources = Array(panel_eval(panel.options[:resources])) %>
|
|
2
|
+
<% variant = (panel.options[:variant] || :default).to_sym %>
|
|
3
|
+
|
|
4
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
|
5
|
+
<div class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
|
|
6
|
+
<h3 class="font-semibold text-slate-900 dark:text-white"><%= panel.title %></h3>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if resources.any? %>
|
|
10
|
+
<% if variant == :portals %>
|
|
11
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
12
|
+
<% resources.each do |portal_item| %>
|
|
13
|
+
<%
|
|
14
|
+
key = portal_item[:key] || portal_item["key"]
|
|
15
|
+
label = portal_item[:label] || portal_item["label"]
|
|
16
|
+
description = portal_item[:description] || portal_item["description"]
|
|
17
|
+
path = portal_item[:path] || portal_item["path"]
|
|
18
|
+
icon = portal_item[:icon] || portal_item["icon"]
|
|
19
|
+
color = (portal_item[:color] || portal_item["color"] || "slate").to_s
|
|
20
|
+
count = portal_item[:count] || portal_item["count"]
|
|
21
|
+
count_value = count.is_a?(Proc) ? panel_eval(count) : count
|
|
22
|
+
%>
|
|
23
|
+
|
|
24
|
+
<%= link_to(path || "#", class: "admin-suite-portal-accent admin-suite-portal-accent--#{color} admin-suite-portal-card group block rounded-xl p-4 transition-all") do %>
|
|
25
|
+
<div class="flex items-start justify-between gap-3">
|
|
26
|
+
<div class="flex items-center justify-center w-10 h-10 rounded-xl admin-suite-portal-chip flex-shrink-0">
|
|
27
|
+
<%= admin_suite_icon(icon.presence || key.to_s, class: "admin-suite-portal-icon w-5 h-5") %>
|
|
28
|
+
</div>
|
|
29
|
+
<% if count_value.present? %>
|
|
30
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-white/70 border border-slate-200/50 text-slate-700">
|
|
31
|
+
<%= count_value %>
|
|
32
|
+
</span>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="mt-3">
|
|
37
|
+
<div class="text-sm font-semibold text-slate-900">
|
|
38
|
+
<%= label %>
|
|
39
|
+
</div>
|
|
40
|
+
<% if description.present? %>
|
|
41
|
+
<div class="mt-1 text-xs text-slate-600 line-clamp-2">
|
|
42
|
+
<%= description %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="admin-suite-portal-link mt-3 inline-flex items-center gap-1 text-sm font-medium">
|
|
48
|
+
Enter Portal
|
|
49
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
50
|
+
</div>
|
|
51
|
+
<% end %>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
<% else %>
|
|
55
|
+
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
56
|
+
<% resources.each do |resource_item| %>
|
|
57
|
+
<%
|
|
58
|
+
if resource_item.is_a?(Hash)
|
|
59
|
+
label = resource_item[:label] || resource_item["label"]
|
|
60
|
+
description = resource_item[:description] || resource_item["description"]
|
|
61
|
+
resource_name = resource_item[:resource_name] || resource_item["resource_name"]
|
|
62
|
+
path = resource_item[:path] || resource_item["path"]
|
|
63
|
+
count = resource_item[:count] || resource_item["count"]
|
|
64
|
+
icon = resource_item[:icon] || resource_item["icon"]
|
|
65
|
+
else
|
|
66
|
+
resource_name = resource_item.to_s
|
|
67
|
+
label = resource_name.humanize
|
|
68
|
+
description = "Resource"
|
|
69
|
+
path = nil
|
|
70
|
+
count = nil
|
|
71
|
+
icon = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
path ||= (resources_path(portal: @portal_key, resource_name: resource_name.to_s) rescue nil)
|
|
75
|
+
path = path.is_a?(Proc) ? panel_eval(path) : path
|
|
76
|
+
count_value = count.is_a?(Proc) ? panel_eval(count) : count
|
|
77
|
+
%>
|
|
78
|
+
<%= link_to(path || "#", class: "admin-suite-theme-border-hover block rounded-lg border border-slate-200 p-4 hover:shadow-sm transition-all group") do %>
|
|
79
|
+
<div class="flex items-center justify-between gap-3">
|
|
80
|
+
<div class="min-w-0">
|
|
81
|
+
<div class="admin-suite-theme-text-hover text-sm font-medium text-slate-900 truncate">
|
|
82
|
+
<span class="inline-flex items-center gap-2">
|
|
83
|
+
<% if icon.present? %>
|
|
84
|
+
<%= admin_suite_icon(icon, class: "w-4 h-4 text-slate-400") %>
|
|
85
|
+
<% end %>
|
|
86
|
+
<%= label %>
|
|
87
|
+
</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="text-xs text-slate-500 truncate"><%= description %></div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="flex items-center gap-2 flex-shrink-0">
|
|
92
|
+
<% if count_value.present? %>
|
|
93
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
|
|
94
|
+
<%= count_value %>
|
|
95
|
+
</span>
|
|
96
|
+
<% end %>
|
|
97
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4 text-slate-300") %>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
101
|
+
<% end %>
|
|
102
|
+
</div>
|
|
103
|
+
<% end %>
|
|
104
|
+
<% else %>
|
|
105
|
+
<div class="p-4 text-sm text-slate-500 dark:text-slate-400">No cards configured.</div>
|
|
106
|
+
<% end %>
|
|
107
|
+
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<% data = Array(panel_eval(panel.options[:data])) %>
|
|
2
|
+
<% total = panel.options[:total] %>
|
|
3
|
+
<% color = (panel.options[:color] || theme_primary).to_sym %>
|
|
4
|
+
<% max_value = (data.map { |d| d[:value].to_f }.max || 1).to_f %>
|
|
5
|
+
<% max_value = 1.0 if max_value.zero? %>
|
|
6
|
+
|
|
7
|
+
<% bar_color = case color
|
|
8
|
+
when :amber then "bg-amber-500 dark:bg-amber-400"
|
|
9
|
+
when :green then "bg-green-500 dark:bg-green-400"
|
|
10
|
+
when :red then "bg-red-500 dark:bg-red-400"
|
|
11
|
+
when :cyan then "bg-cyan-500 dark:bg-cyan-400"
|
|
12
|
+
when :violet then "bg-violet-500 dark:bg-violet-400"
|
|
13
|
+
when :indigo then "bg-indigo-500 dark:bg-indigo-400"
|
|
14
|
+
else "bg-indigo-500 dark:bg-indigo-400"
|
|
15
|
+
end %>
|
|
16
|
+
|
|
17
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
|
|
18
|
+
<div class="flex items-center justify-between mb-4">
|
|
19
|
+
<h3 class="font-semibold text-slate-900 dark:text-white"><%= panel.title %></h3>
|
|
20
|
+
<% if total.present? %>
|
|
21
|
+
<span class="text-2xl font-bold text-slate-900 dark:text-white"><%= total %></span>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<% if data.any? %>
|
|
26
|
+
<div class="flex items-end gap-1 h-16">
|
|
27
|
+
<% data.each do |d| %>
|
|
28
|
+
<% value = d[:value].to_f %>
|
|
29
|
+
<% height_float = (value / max_value) * 100.0 %>
|
|
30
|
+
<% height_float = 0.0 if height_float.nan? || height_float.infinite? %>
|
|
31
|
+
<% height = height_float.round %>
|
|
32
|
+
<div class="flex-1 flex flex-col items-center gap-1">
|
|
33
|
+
<div class="w-full rounded-t <%= bar_color %> transition-all" style="height: <%= height %>%"
|
|
34
|
+
title="<%= "#{d[:label]}: #{d[:value]}" %>"></div>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="flex gap-1 mt-2">
|
|
40
|
+
<% data.each do |d| %>
|
|
41
|
+
<div class="flex-1 text-center text-xs text-slate-400 dark:text-slate-500"><%= d[:label].to_s.first(3) %></div>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
<% else %>
|
|
45
|
+
<div class="p-4 text-sm text-slate-500 dark:text-slate-400">No chart data.</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<% status = (panel_eval(panel.options[:status]) || :unknown).to_sym %>
|
|
2
|
+
<% metrics = panel_eval(panel.options[:metrics]) || {} %>
|
|
3
|
+
|
|
4
|
+
<% status_config = case status
|
|
5
|
+
when :healthy
|
|
6
|
+
{ border: "border-green-200 dark:border-green-800/50",
|
|
7
|
+
badge: "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400",
|
|
8
|
+
dot: "bg-green-500 animate-pulse" }
|
|
9
|
+
when :degraded
|
|
10
|
+
{ border: "border-amber-200 dark:border-amber-800/50",
|
|
11
|
+
badge: "bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400",
|
|
12
|
+
dot: "bg-amber-500 animate-pulse" }
|
|
13
|
+
when :critical
|
|
14
|
+
{ border: "border-red-200 dark:border-red-800/50",
|
|
15
|
+
badge: "bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400",
|
|
16
|
+
dot: "bg-red-500 animate-pulse" }
|
|
17
|
+
else
|
|
18
|
+
{ border: "border-slate-200 dark:border-slate-700",
|
|
19
|
+
badge: "bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400",
|
|
20
|
+
dot: "bg-slate-400" }
|
|
21
|
+
end %>
|
|
22
|
+
|
|
23
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border <%= status_config[:border] %> overflow-hidden">
|
|
24
|
+
<div class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
|
|
25
|
+
<h3 class="font-semibold text-slate-900 dark:text-white"><%= panel.title %></h3>
|
|
26
|
+
<span class="flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium <%= status_config[:badge] %>">
|
|
27
|
+
<span class="w-2 h-2 rounded-full <%= status_config[:dot] %>"></span>
|
|
28
|
+
<%= status.to_s.humanize %>
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<% if metrics.present? %>
|
|
33
|
+
<div class="p-4 grid grid-cols-2 gap-3">
|
|
34
|
+
<% metrics.each do |key, val| %>
|
|
35
|
+
<div>
|
|
36
|
+
<div class="text-lg font-semibold text-slate-900 dark:text-white"><%= val %></div>
|
|
37
|
+
<div class="text-xs text-slate-500 dark:text-slate-400"><%= key.to_s.humanize %></div>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
<% else %>
|
|
42
|
+
<div class="p-4 text-sm text-slate-500 dark:text-slate-400">No metrics.</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<% scope = panel.options[:scope] %>
|
|
2
|
+
<% items = panel_eval(scope) %>
|
|
3
|
+
<% items = items.limit(panel.options[:limit]) if items.respond_to?(:limit) && panel.options[:limit].present? %>
|
|
4
|
+
<% items = Array(items) %>
|
|
5
|
+
<% empty_message = panel.options[:empty_message] || "No recent items" %>
|
|
6
|
+
<% view_all_path = panel_eval(panel.options[:view_all_path]) %>
|
|
7
|
+
<% link_proc = panel.options[:link] %>
|
|
8
|
+
|
|
9
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
|
10
|
+
<div class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
|
|
11
|
+
<h3 class="font-semibold text-slate-900 dark:text-white"><%= panel.title %></h3>
|
|
12
|
+
<% if view_all_path.present? %>
|
|
13
|
+
<%= link_to "View all →", view_all_path, class: "text-sm #{theme_link_class}" %>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<% if items.any? %>
|
|
18
|
+
<ul class="divide-y divide-slate-100 dark:divide-slate-700">
|
|
19
|
+
<% items.each do |item| %>
|
|
20
|
+
<% path =
|
|
21
|
+
if link_proc.respond_to?(:call)
|
|
22
|
+
link_proc.arity == 1 ? link_proc.call(item) : link_proc.call
|
|
23
|
+
else
|
|
24
|
+
auto_admin_suite_path_for(item)
|
|
25
|
+
end
|
|
26
|
+
%>
|
|
27
|
+
<li class="px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
|
28
|
+
<% title = item_display_title(item) rescue item.to_s %>
|
|
29
|
+
<% subtitle = (item.respond_to?(:created_at) && item.created_at) ? "#{time_ago_in_words(item.created_at)} ago" : nil %>
|
|
30
|
+
<% if path.present? %>
|
|
31
|
+
<%= link_to path, class: "flex items-center justify-between gap-3" do %>
|
|
32
|
+
<div class="min-w-0">
|
|
33
|
+
<div class="text-sm font-medium text-slate-900 dark:text-white truncate"><%= title.to_s.truncate(48) %></div>
|
|
34
|
+
<% if subtitle.present? %>
|
|
35
|
+
<div class="text-xs text-slate-500 dark:text-slate-400"><%= subtitle %></div>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
38
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4 text-slate-300 dark:text-slate-600 flex-shrink-0") %>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% else %>
|
|
41
|
+
<div class="flex items-center justify-between gap-3">
|
|
42
|
+
<div class="min-w-0">
|
|
43
|
+
<div class="text-sm font-medium text-slate-900 dark:text-white truncate"><%= title.to_s.truncate(48) %></div>
|
|
44
|
+
<% if subtitle.present? %>
|
|
45
|
+
<div class="text-xs text-slate-500 dark:text-slate-400"><%= subtitle %></div>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
</li>
|
|
51
|
+
<% end %>
|
|
52
|
+
</ul>
|
|
53
|
+
<% else %>
|
|
54
|
+
<div class="p-4 text-center text-sm text-slate-500 dark:text-slate-400"><%= empty_message %></div>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<% value = panel_eval(panel.options[:value]) %>
|
|
2
|
+
<% color = (panel.options[:color] || :default).to_sym %>
|
|
3
|
+
<% subtitle = panel.options[:subtitle] %>
|
|
4
|
+
<% trend = panel.options[:trend] %>
|
|
5
|
+
<% trend_direction = (panel.options[:trend_direction] || :neutral).to_sym %>
|
|
6
|
+
<% variant = (panel.options[:variant] || :default).to_sym %>
|
|
7
|
+
|
|
8
|
+
<% color_classes = case color
|
|
9
|
+
when :green
|
|
10
|
+
{ bg: "bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20",
|
|
11
|
+
border: "border border-green-200 dark:border-green-800/50",
|
|
12
|
+
text: "text-green-700 dark:text-green-400",
|
|
13
|
+
label: "text-green-600 dark:text-green-500" }
|
|
14
|
+
when :red
|
|
15
|
+
{ bg: "bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20",
|
|
16
|
+
border: "border border-red-200 dark:border-red-800/50",
|
|
17
|
+
text: "text-red-700 dark:text-red-400",
|
|
18
|
+
label: "text-red-600 dark:text-red-500" }
|
|
19
|
+
when :amber
|
|
20
|
+
{ bg: "bg-gradient-to-br from-amber-50 to-amber-100 dark:from-amber-900/20 dark:to-amber-800/20",
|
|
21
|
+
border: "border border-amber-200 dark:border-amber-800/50",
|
|
22
|
+
text: "text-amber-700 dark:text-amber-400",
|
|
23
|
+
label: "text-amber-600 dark:text-amber-500" }
|
|
24
|
+
when :cyan
|
|
25
|
+
{ bg: "bg-gradient-to-br from-cyan-50 to-cyan-100 dark:from-cyan-900/20 dark:to-cyan-800/20",
|
|
26
|
+
border: "border border-cyan-200 dark:border-cyan-800/50",
|
|
27
|
+
text: "text-cyan-700 dark:text-cyan-400",
|
|
28
|
+
label: "text-cyan-600 dark:text-cyan-500" }
|
|
29
|
+
when :violet
|
|
30
|
+
{ bg: "bg-gradient-to-br from-violet-50 to-violet-100 dark:from-violet-900/20 dark:to-violet-800/20",
|
|
31
|
+
border: "border border-violet-200 dark:border-violet-800/50",
|
|
32
|
+
text: "text-violet-700 dark:text-violet-400",
|
|
33
|
+
label: "text-violet-600 dark:text-violet-500" }
|
|
34
|
+
else
|
|
35
|
+
{ bg: "bg-white dark:bg-slate-800",
|
|
36
|
+
border: "border border-slate-200 dark:border-slate-700",
|
|
37
|
+
text: "text-slate-900 dark:text-white",
|
|
38
|
+
label: "text-slate-500 dark:text-slate-400" }
|
|
39
|
+
end %>
|
|
40
|
+
|
|
41
|
+
<% if variant == :mini %>
|
|
42
|
+
<div class="<%= color_classes[:bg] %> rounded-xl p-4 <%= color_classes[:border] %>">
|
|
43
|
+
<div class="text-2xl font-bold <%= color_classes[:text] %>"><%= value %></div>
|
|
44
|
+
<div class="text-sm <%= color_classes[:label] %>"><%= panel.title %></div>
|
|
45
|
+
</div>
|
|
46
|
+
<% else %>
|
|
47
|
+
<div class="<%= color_classes[:bg] %> rounded-xl p-5 <%= color_classes[:border] %>">
|
|
48
|
+
<div class="flex items-center justify-between">
|
|
49
|
+
<div class="text-3xl font-bold <%= color_classes[:text] %>"><%= value %></div>
|
|
50
|
+
<% if trend.present? %>
|
|
51
|
+
<% trend_class = case trend_direction
|
|
52
|
+
when :up then "text-green-500"
|
|
53
|
+
when :down then "text-red-500"
|
|
54
|
+
else "text-slate-400"
|
|
55
|
+
end %>
|
|
56
|
+
<span class="text-sm font-medium <%= trend_class %>"><%= trend %></span>
|
|
57
|
+
<% end %>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="text-sm <%= color_classes[:label] %> mt-1"><%= panel.title %></div>
|
|
60
|
+
<% if subtitle.present? %>
|
|
61
|
+
<div class="text-xs <%= color_classes[:label] %> mt-1 opacity-75"><%= subtitle %></div>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<% rows_value = panel_eval(panel.options[:rows]) %>
|
|
2
|
+
<% rows = Array(rows_value) %>
|
|
3
|
+
<% columns = Array(panel.options[:columns]).presence %>
|
|
4
|
+
|
|
5
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
|
6
|
+
<div class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
|
|
7
|
+
<h3 class="font-semibold text-slate-900 dark:text-white"><%= panel.title %></h3>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<% if rows.any? %>
|
|
11
|
+
<% columns ||= rows.first.respond_to?(:attributes) ? rows.first.attributes.keys.first(6).map(&:to_sym) : [] %>
|
|
12
|
+
<div class="overflow-x-auto">
|
|
13
|
+
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
|
|
14
|
+
<thead class="bg-slate-50/50 dark:bg-slate-900/30">
|
|
15
|
+
<tr>
|
|
16
|
+
<% columns.each do |col| %>
|
|
17
|
+
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider"><%= col.to_s.humanize %></th>
|
|
18
|
+
<% end %>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
|
|
22
|
+
<% rows.each do |row| %>
|
|
23
|
+
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-900/30">
|
|
24
|
+
<% columns.each do |col| %>
|
|
25
|
+
<% val = row.respond_to?(col) ? (row.public_send(col) rescue nil) : (row[col] rescue nil) %>
|
|
26
|
+
<td class="px-4 py-3 text-sm text-slate-700 dark:text-slate-200"><%= format_table_cell(val) rescue val.to_s %></td>
|
|
27
|
+
<% end %>
|
|
28
|
+
</tr>
|
|
29
|
+
<% end %>
|
|
30
|
+
</tbody>
|
|
31
|
+
</table>
|
|
32
|
+
</div>
|
|
33
|
+
<% else %>
|
|
34
|
+
<div class="p-4 text-sm text-slate-500 dark:text-slate-400">No rows.</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<% content_for :title, "#{@portal[:label]} - Admin Suite" %>
|
|
2
|
+
|
|
3
|
+
<div class="px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
|
|
4
|
+
<!-- Header -->
|
|
5
|
+
<div class="mb-8">
|
|
6
|
+
<nav class="flex items-center gap-2 text-sm text-slate-500 mb-2">
|
|
7
|
+
<%= link_to "Admin Suite", root_path, class: theme_link_hover_text_class %>
|
|
8
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
9
|
+
<span class="text-slate-900"><%= @portal[:label] %></span>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<div class="flex items-center justify-between gap-4">
|
|
13
|
+
<div class="flex items-center gap-3">
|
|
14
|
+
<% color = portal_color(@portal_key) %>
|
|
15
|
+
<div class="admin-suite-portal-accent admin-suite-portal-accent--<%= color %> admin-suite-portal-chip flex items-center justify-center w-10 h-10 rounded-xl">
|
|
16
|
+
<%= portal_icon(@portal_key, class: "admin-suite-portal-icon w-5 h-5") %>
|
|
17
|
+
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<h1 class="text-2xl font-bold text-slate-900"><%= @portal[:label] %></h1>
|
|
20
|
+
<% if @portal[:description].present? %>
|
|
21
|
+
<p class="text-sm text-slate-500"><%= @portal[:description] %></p>
|
|
22
|
+
<% end %>
|
|
23
|
+
<p class="text-sm text-slate-500">
|
|
24
|
+
<%= @sections.sum { |(_k, s)| s[:items].size } %> resources across <%= @sections.size %> sections
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<%= link_to "All portals", root_path, class: "px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors" %>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<% if @dashboard_rows.present? %>
|
|
34
|
+
<!-- Dashboard (DSL-driven) -->
|
|
35
|
+
<%= render_dashboard_rows(@dashboard_rows) %>
|
|
36
|
+
<% else %>
|
|
37
|
+
<!-- Sections (fallback) -->
|
|
38
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
39
|
+
<% @sections.each do |_section_key, section| %>
|
|
40
|
+
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
41
|
+
<div class="px-6 py-4 border-b border-slate-200 bg-slate-50 flex items-center justify-between">
|
|
42
|
+
<h2 class="font-semibold text-slate-900"><%= section[:label] %></h2>
|
|
43
|
+
<span class="text-xs font-semibold px-2 py-0.5 rounded-full <%= theme_badge_primary_class %>">
|
|
44
|
+
<%= section[:items].size %>
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<% if section[:items].any? %>
|
|
49
|
+
<div class="divide-y divide-slate-100">
|
|
50
|
+
<% section[:items].each do |item| %>
|
|
51
|
+
<%= link_to item[:path], class: "block px-6 py-4 hover:bg-slate-50 transition-colors group", data: { turbo_frame: "_top" } do %>
|
|
52
|
+
<div class="flex items-center justify-between gap-3">
|
|
53
|
+
<div class="min-w-0">
|
|
54
|
+
<div class="admin-suite-theme-text-hover text-sm font-medium text-slate-900 truncate">
|
|
55
|
+
<%= item[:label] %>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="text-xs text-slate-500">
|
|
58
|
+
<%= item[:resource]&.model_class&.name || "Resource" %>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<%= admin_suite_icon("chevron-right", class: "w-5 h-5 text-slate-300 flex-shrink-0") %>
|
|
62
|
+
</div>
|
|
63
|
+
<% end %>
|
|
64
|
+
<% end %>
|
|
65
|
+
</div>
|
|
66
|
+
<% else %>
|
|
67
|
+
<div class="p-10 text-center text-sm text-slate-500">
|
|
68
|
+
No resources in this section yet.
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</div>
|
|
72
|
+
<% end %>
|
|
73
|
+
</div>
|
|
74
|
+
<% end %>
|
|
75
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%= form_with model: resource,
|
|
2
|
+
url: resource.new_record? ? resources_path(portal: current_portal, resource_name: resource_name) : resource_path(portal: current_portal, resource_name: resource_name, id: resource.to_param),
|
|
3
|
+
local: true,
|
|
4
|
+
class: "space-y-4" do |f| %>
|
|
5
|
+
|
|
6
|
+
<% if resource.errors.any? %>
|
|
7
|
+
<div class="p-4 bg-red-50 border border-red-200 text-red-800 rounded-lg">
|
|
8
|
+
<div class="font-medium mb-2"><%= pluralize(resource.errors.count, "error") %> prevented saving:</div>
|
|
9
|
+
<ul class="list-disc list-inside text-sm">
|
|
10
|
+
<% resource.errors.full_messages.each do |msg| %>
|
|
11
|
+
<li><%= msg %></li>
|
|
12
|
+
<% end %>
|
|
13
|
+
</ul>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<% columns = resource.class.column_names.reject { |c| %w[id created_at updated_at].include?(c) } %>
|
|
18
|
+
<% columns.each do |column| %>
|
|
19
|
+
<div>
|
|
20
|
+
<%= f.label column, class: "block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1" %>
|
|
21
|
+
<%= f.text_field column, class: "form-input w-full" %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<div class="pt-4 flex items-center gap-2">
|
|
26
|
+
<%= f.button (resource.new_record? ? "Create" : "Save"),
|
|
27
|
+
class: "admin-suite-btn-primary text-sm font-medium rounded-lg",
|
|
28
|
+
data: { disable_with: "Saving..." } %>
|
|
29
|
+
<%= link_to "Cancel", resource.new_record? ? resources_path(portal: current_portal, resource_name: resource_name) : resource_path(portal: current_portal, resource_name: resource_name, id: resource.to_param),
|
|
30
|
+
class: "px-4 py-2 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-100" %>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<% content_for :title, "Edit #{resource_config.human_name} - Admin Suite" %>
|
|
2
|
+
|
|
3
|
+
<div class="px-4 sm:px-6 lg:px-8 max-w-3xl mx-auto">
|
|
4
|
+
<!-- Header -->
|
|
5
|
+
<div class="mb-6">
|
|
6
|
+
<nav class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 mb-2">
|
|
7
|
+
<%= link_to "Dashboard", root_path, class: theme_link_hover_text_class %>
|
|
8
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
9
|
+
<%= link_to resource_config.human_name_plural, url_for(action: :index), class: theme_link_hover_text_class %>
|
|
10
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
11
|
+
<%= link_to @resource.respond_to?(:name) ? @resource.name.truncate(20) : "##{@resource.id}", url_for(action: :show, id: @resource.id), class: theme_link_hover_text_class %>
|
|
12
|
+
<%= admin_suite_icon("chevron-right", class: "w-4 h-4") %>
|
|
13
|
+
<span class="text-slate-900 dark:text-white">Edit</span>
|
|
14
|
+
</nav>
|
|
15
|
+
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">Edit <%= resource_config.human_name %></h1>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Form -->
|
|
19
|
+
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
|
20
|
+
<div class="p-6">
|
|
21
|
+
<%= render "admin_suite/shared/form", resource: @resource %>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|