easy-admin-rails 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/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/builds/easy_admin.base.js +43505 -0
- data/app/assets/builds/easy_admin.base.js.map +7 -0
- data/app/assets/builds/easy_admin.css +6141 -0
- data/app/assets/config/easy_admin_manifest.js +1 -0
- data/app/assets/images/jsoneditor-icons.svg +749 -0
- data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
- data/app/components/easy_admin/base_component.rb +35 -0
- data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
- data/app/components/easy_admin/batch_action_form_component.rb +124 -0
- data/app/components/easy_admin/combined_filters_component.rb +232 -0
- data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
- data/app/components/easy_admin/context_menu_component.rb +161 -0
- data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
- data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
- data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
- data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
- data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
- data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
- data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
- data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
- data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
- data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
- data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
- data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
- data/app/components/easy_admin/dashboards/show_component.rb +163 -0
- data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
- data/app/components/easy_admin/date_picker_component.rb +188 -0
- data/app/components/easy_admin/fields/base_component.rb +101 -0
- data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
- data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
- data/app/components/easy_admin/fields/form/date_component.rb +55 -0
- data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
- data/app/components/easy_admin/fields/form/email_component.rb +55 -0
- data/app/components/easy_admin/fields/form/file_component.rb +190 -0
- data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
- data/app/components/easy_admin/fields/form/json_component.rb +81 -0
- data/app/components/easy_admin/fields/form/number_component.rb +55 -0
- data/app/components/easy_admin/fields/form/select_component.rb +326 -0
- data/app/components/easy_admin/fields/form/text_component.rb +55 -0
- data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
- data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
- data/app/components/easy_admin/fields/index/date_component.rb +13 -0
- data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/index/email_component.rb +24 -0
- data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
- data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
- data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
- data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
- data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
- data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
- data/app/components/easy_admin/fields/index/json_component.rb +23 -0
- data/app/components/easy_admin/fields/index/number_component.rb +20 -0
- data/app/components/easy_admin/fields/index/select_component.rb +25 -0
- data/app/components/easy_admin/fields/index/text_component.rb +20 -0
- data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
- data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
- data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
- data/app/components/easy_admin/fields/show/date_component.rb +13 -0
- data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/show/email_component.rb +19 -0
- data/app/components/easy_admin/fields/show/file_component.rb +304 -0
- data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
- data/app/components/easy_admin/fields/show/json_component.rb +45 -0
- data/app/components/easy_admin/fields/show/number_component.rb +20 -0
- data/app/components/easy_admin/fields/show/select_component.rb +25 -0
- data/app/components/easy_admin/fields/show/text_component.rb +17 -0
- data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
- data/app/components/easy_admin/filters_component.rb +120 -0
- data/app/components/easy_admin/form_tabs_component.rb +166 -0
- data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
- data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
- data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
- data/app/components/easy_admin/modal_frame_component.rb +26 -0
- data/app/components/easy_admin/navbar_component.rb +226 -0
- data/app/components/easy_admin/notification_component.rb +83 -0
- data/app/components/easy_admin/pagination_component.rb +188 -0
- data/app/components/easy_admin/quick_filters_component.rb +65 -0
- data/app/components/easy_admin/resource_pagination_component.rb +14 -0
- data/app/components/easy_admin/resources/index_component.rb +211 -0
- data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
- data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
- data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
- data/app/components/easy_admin/resources/table_component.rb +206 -0
- data/app/components/easy_admin/resources/table_row_component.rb +160 -0
- data/app/components/easy_admin/row_action_form_component.rb +127 -0
- data/app/components/easy_admin/scopes_component.rb +224 -0
- data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
- data/app/components/easy_admin/show_layout_component.rb +600 -0
- data/app/components/easy_admin/sidebar_component.rb +174 -0
- data/app/components/easy_admin/turbo/response_component.rb +40 -0
- data/app/components/easy_admin/turbo/stream_component.rb +28 -0
- data/app/controllers/easy_admin/application_controller.rb +66 -0
- data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
- data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
- data/app/controllers/easy_admin/passwords_controller.rb +15 -0
- data/app/controllers/easy_admin/registrations_controller.rb +52 -0
- data/app/controllers/easy_admin/resources_controller.rb +907 -0
- data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
- data/app/controllers/easy_admin/sessions_controller.rb +32 -0
- data/app/controllers/easy_admin/settings_controller.rb +94 -0
- data/app/helpers/easy_admin/application_helper.rb +4 -0
- data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
- data/app/helpers/easy_admin/fields_helper.rb +27 -0
- data/app/helpers/easy_admin/pagy_helper.rb +30 -0
- data/app/helpers/easy_admin/resources_helper.rb +39 -0
- data/app/javascript/easy_admin/application.js +12 -0
- data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
- data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
- data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
- data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
- data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
- data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
- data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
- data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
- data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
- data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
- data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
- data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
- data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
- data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
- data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
- data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
- data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
- data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
- data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
- data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
- data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
- data/app/javascript/easy_admin/controllers.js +54 -0
- data/app/javascript/easy_admin.base.js +4 -0
- data/app/models/easy_admin/admin_user.rb +53 -0
- data/app/models/easy_admin/application_record.rb +5 -0
- data/app/views/easy_admin/dashboard/index.html.erb +3 -0
- data/app/views/easy_admin/dashboards/show.html.erb +7 -0
- data/app/views/easy_admin/passwords/edit.html.erb +42 -0
- data/app/views/easy_admin/passwords/new.html.erb +41 -0
- data/app/views/easy_admin/registrations/new.html.erb +65 -0
- data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
- data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
- data/app/views/easy_admin/resources/edit.html.erb +151 -0
- data/app/views/easy_admin/resources/index.html.erb +12 -0
- data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
- data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
- data/app/views/easy_admin/resources/new.html.erb +100 -0
- data/app/views/easy_admin/resources/show.html.erb +31 -0
- data/app/views/easy_admin/sessions/new.html.erb +55 -0
- data/app/views/easy_admin/settings/_form.html.erb +51 -0
- data/app/views/easy_admin/settings/index.html.erb +53 -0
- data/app/views/layouts/easy_admin/application.html.erb +48 -0
- data/app/views/layouts/easy_admin/auth.html.erb +34 -0
- data/config/initializers/easy_admin_card_factory.rb +27 -0
- data/config/initializers/pagy.rb +15 -0
- data/config/initializers/rack_mini_profiler.rb +67 -0
- data/config/routes.rb +70 -0
- data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
- data/lib/easy-admin.rb +32 -0
- data/lib/easy_admin/action.rb +159 -0
- data/lib/easy_admin/batch_action.rb +134 -0
- data/lib/easy_admin/configuration.rb +75 -0
- data/lib/easy_admin/dashboard.rb +110 -0
- data/lib/easy_admin/dashboard_registry.rb +30 -0
- data/lib/easy_admin/delete_action.rb +22 -0
- data/lib/easy_admin/engine.rb +54 -0
- data/lib/easy_admin/field.rb +118 -0
- data/lib/easy_admin/resource.rb +806 -0
- data/lib/easy_admin/resource_registry.rb +22 -0
- data/lib/easy_admin/types/json_type.rb +25 -0
- data/lib/easy_admin/version.rb +3 -0
- data/lib/generators/easy_admin/auth_generator.rb +69 -0
- data/lib/generators/easy_admin/card/card_generator.rb +94 -0
- data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
- data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
- data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
- data/lib/generators/easy_admin/install_generator.rb +25 -0
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
- data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
- data/lib/generators/easy_admin/resource_generator.rb +43 -0
- data/lib/generators/easy_admin/templates/AUTH_README +35 -0
- data/lib/generators/easy_admin/templates/README +27 -0
- data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
- data/lib/generators/easy_admin/templates/devise.rb +267 -0
- data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
- data/lib/generators/easy_admin/templates/resource.rb +29 -0
- data/lib/tasks/easy_admin_tasks.rake +4 -0
- metadata +445 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
module Cards
|
4
|
+
class TableCardComponent < BaseCardComponent
|
5
|
+
private
|
6
|
+
|
7
|
+
def render_card_content
|
8
|
+
div(class: "flex flex-col h-full") do
|
9
|
+
# Table Content
|
10
|
+
div(class: "flex-1 overflow-auto") do
|
11
|
+
if card_data[:rows] && card_data[:rows].any?
|
12
|
+
render_table
|
13
|
+
else
|
14
|
+
render_no_data_state
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Footer
|
19
|
+
if card_data[:show_view_all] && card_data[:view_all_url]
|
20
|
+
render_table_footer
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def render_table
|
26
|
+
table(class: "min-w-full divide-y divide-gray-200") do
|
27
|
+
# Table Header
|
28
|
+
if card_data[:headers]
|
29
|
+
thead(class: "bg-gray-50") do
|
30
|
+
tr do
|
31
|
+
card_data[:headers].each do |header|
|
32
|
+
th(
|
33
|
+
class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",
|
34
|
+
scope: "col"
|
35
|
+
) { header }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Table Body
|
42
|
+
tbody(class: "bg-white divide-y divide-gray-200") do
|
43
|
+
card_data[:rows].each_with_index do |row, index|
|
44
|
+
tr(class: index.even? ? "bg-white" : "bg-gray-50") do
|
45
|
+
row.each do |cell|
|
46
|
+
td(class: "px-6 py-4 whitespace-nowrap text-sm") do
|
47
|
+
render_table_cell(cell)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def render_table_cell(cell)
|
57
|
+
case cell
|
58
|
+
when Hash
|
59
|
+
case cell[:type]
|
60
|
+
when :link
|
61
|
+
a(
|
62
|
+
href: cell[:url],
|
63
|
+
class: "text-blue-600 hover:text-blue-800 font-medium"
|
64
|
+
) { cell[:text] }
|
65
|
+
when :badge
|
66
|
+
span(class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium #{badge_classes(cell[:variant])}") do
|
67
|
+
cell[:text]
|
68
|
+
end
|
69
|
+
when :avatar
|
70
|
+
div(class: "flex items-center") do
|
71
|
+
if cell[:image_url]
|
72
|
+
img(
|
73
|
+
class: "h-8 w-8 rounded-full",
|
74
|
+
src: cell[:image_url],
|
75
|
+
alt: cell[:alt] || ""
|
76
|
+
)
|
77
|
+
else
|
78
|
+
div(class: "h-8 w-8 rounded-full bg-gray-300 flex items-center justify-center") do
|
79
|
+
span(class: "text-xs font-medium text-gray-700") do
|
80
|
+
cell[:initials] || "?"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
if cell[:text]
|
85
|
+
span(class: "ml-2 text-gray-900") { cell[:text] }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
when :progress
|
89
|
+
div(class: "w-full bg-gray-200 rounded-full h-2") do
|
90
|
+
div(
|
91
|
+
class: "bg-blue-600 h-2 rounded-full",
|
92
|
+
style: "width: #{[cell[:value] || 0, 100].min}%"
|
93
|
+
)
|
94
|
+
end
|
95
|
+
else
|
96
|
+
span(class: "text-gray-900") { cell[:text] || cell.to_s }
|
97
|
+
end
|
98
|
+
else
|
99
|
+
span(class: "text-gray-900") { cell.to_s }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def badge_classes(variant)
|
104
|
+
case variant&.to_s
|
105
|
+
when 'success'
|
106
|
+
"bg-green-100 text-green-800"
|
107
|
+
when 'warning'
|
108
|
+
"bg-yellow-100 text-yellow-800"
|
109
|
+
when 'danger', 'error'
|
110
|
+
"bg-red-100 text-red-800"
|
111
|
+
when 'info'
|
112
|
+
"bg-blue-100 text-blue-800"
|
113
|
+
else
|
114
|
+
"bg-gray-100 text-gray-800"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def render_no_data_state
|
119
|
+
div(class: "flex flex-col items-center justify-center py-8 text-gray-500") do
|
120
|
+
unsafe_raw <<~SVG
|
121
|
+
<svg class="w-12 h-12 mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
122
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
123
|
+
</svg>
|
124
|
+
SVG
|
125
|
+
p(class: "text-sm") { "No table data available" }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def render_table_footer
|
130
|
+
div(class: "px-6 py-3 bg-gray-50 border-t border-gray-200") do
|
131
|
+
div(class: "flex items-center justify-between") do
|
132
|
+
div(class: "text-sm text-gray-700") do
|
133
|
+
if card_data[:total_count] && card_data[:total_count] > 0
|
134
|
+
"Showing #{card_data[:rows].length} of #{number_with_delimiter(card_data[:total_count])} entries"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
a(
|
139
|
+
href: card_data[:view_all_url],
|
140
|
+
class: "text-sm text-blue-600 hover:text-blue-800 font-medium"
|
141
|
+
) { "View all" }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
class ChartCardComponent < BaseCardComponent
|
4
|
+
private
|
5
|
+
|
6
|
+
def render_card_actions
|
7
|
+
if card[:chart_type] && card[:allow_chart_type_change]
|
8
|
+
select(class: "text-sm border-gray-200 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500", "aria-label": "Chart type") do
|
9
|
+
option("Line", value: "line", selected: card[:chart_type] == :line)
|
10
|
+
option("Area", value: "area", selected: card[:chart_type] == :area)
|
11
|
+
option("Bar", value: "bar", selected: card[:chart_type] == :bar)
|
12
|
+
option("Horizontal Bar", value: "horizontal_bar", selected: card[:chart_type] == :horizontal_bar)
|
13
|
+
option("Pie", value: "pie", selected: card[:chart_type] == :pie)
|
14
|
+
option("Donut", value: "donut", selected: card[:chart_type] == :donut)
|
15
|
+
option("Scatter", value: "scatter", selected: card[:chart_type] == :scatter)
|
16
|
+
option("Bubble", value: "bubble", selected: card[:chart_type] == :bubble)
|
17
|
+
option("Radar", value: "radar", selected: card[:chart_type] == :radar)
|
18
|
+
option("Polar Area", value: "polar_area", selected: card[:chart_type] == :polar_area)
|
19
|
+
option("Mixed", value: "mixed", selected: card[:chart_type] == :mixed)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if card[:time_period_options]
|
24
|
+
select(class: "text-sm border-gray-200 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500 ml-2", "aria-label": "Time period") do
|
25
|
+
card[:time_period_options].each do |option|
|
26
|
+
option(option[:label], value: option[:value], selected: option[:selected])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def render_skeleton
|
33
|
+
div(class: "animate-pulse") do
|
34
|
+
div(class: "bg-gray-200 rounded", style: "height: #{chart_height}px;")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def chart_height
|
39
|
+
base_height = 200
|
40
|
+
card[:rows] > 1 ? base_height * card[:rows] : base_height
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
class MetricCardComponent < BaseCardComponent
|
4
|
+
def view_template
|
5
|
+
div(
|
6
|
+
class: card_classes,
|
7
|
+
role: "region",
|
8
|
+
"aria-labelledby": card_title_id
|
9
|
+
) do
|
10
|
+
render_card_header
|
11
|
+
render_card_body
|
12
|
+
render_delta_footer if card[:show_delta]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def render_delta_footer
|
19
|
+
return unless data && data[:delta]
|
20
|
+
|
21
|
+
div(class: "px-6 py-4 bg-gray-50/30 border-t border-gray-100") do
|
22
|
+
div(class: "flex items-center justify-between") do
|
23
|
+
span(class: delta_classes) do
|
24
|
+
span(class: "text-xs") { plain delta_icon }
|
25
|
+
plain " #{data[:delta]}%"
|
26
|
+
end
|
27
|
+
span(class: "text-xs text-gray-500") do
|
28
|
+
plain "vs #{data[:compare_period] || 'last period'}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def delta_classes
|
35
|
+
base_classes = "inline-flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium"
|
36
|
+
if data[:delta].to_f > 0
|
37
|
+
"#{base_classes} bg-green-100 text-green-800"
|
38
|
+
elsif data[:delta].to_f < 0
|
39
|
+
"#{base_classes} bg-red-100 text-red-800"
|
40
|
+
else
|
41
|
+
"#{base_classes} bg-gray-100 text-gray-600"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def delta_icon
|
46
|
+
if data[:delta].to_f > 0
|
47
|
+
"↗"
|
48
|
+
elsif data[:delta].to_f < 0
|
49
|
+
"↘"
|
50
|
+
else
|
51
|
+
"→"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
class RefreshStreamComponent < BaseComponent
|
4
|
+
def initialize(dashboard_class:, dashboard_instance:, card_name: nil)
|
5
|
+
@dashboard_class = dashboard_class
|
6
|
+
@dashboard_instance = dashboard_instance
|
7
|
+
@card_name = card_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
render EasyAdmin::Turbo::ResponseComponent.new do |s|
|
12
|
+
if @card_name
|
13
|
+
# Refresh specific card
|
14
|
+
refresh_single_card(s, @card_name)
|
15
|
+
else
|
16
|
+
# Refresh all cards
|
17
|
+
@dashboard_class.visible_cards.each do |card|
|
18
|
+
refresh_single_card(s, card[:name])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def refresh_single_card(stream, card_name)
|
27
|
+
card = @dashboard_class.find_card(card_name)
|
28
|
+
return unless card
|
29
|
+
|
30
|
+
card_data = @dashboard_instance.card_data(card_name)
|
31
|
+
|
32
|
+
stream.replace("card_#{card_name}") do
|
33
|
+
render_card_frame(card, card_data)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_card_frame(card, card_data)
|
38
|
+
turbo_frame(id: "card_#{card[:name]}") do
|
39
|
+
case card[:type]
|
40
|
+
when :metric
|
41
|
+
render_metric_card(card, card_data)
|
42
|
+
when :chart
|
43
|
+
render_chart_card(card, card_data)
|
44
|
+
when :table
|
45
|
+
render_table_card(card, card_data)
|
46
|
+
else
|
47
|
+
render_default_card(card, card_data)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_metric_card(card, card_data)
|
53
|
+
div(class: "p-6 flex flex-col h-full") do
|
54
|
+
# Main Metric Value with Trend Color
|
55
|
+
div(class: "flex items-start justify-between mb-4") do
|
56
|
+
div(class: "flex-1") do
|
57
|
+
div(class: metric_value_classes(card_data)) do
|
58
|
+
number_with_delimiter(card_data[:value] || 0)
|
59
|
+
end
|
60
|
+
div(class: "text-sm text-gray-600 font-medium") do
|
61
|
+
card[:description] || card[:title]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Trend Indicator
|
66
|
+
if card_data[:delta]
|
67
|
+
div(class: trend_indicator_classes(card_data)) do
|
68
|
+
trend_icon(card_data)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Sparkline
|
74
|
+
if card[:show_sparkline] && card_data[:sparkline_data]
|
75
|
+
div(class: "flex-1 mb-4") do
|
76
|
+
render_sparkline(card, card_data)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Delta Information
|
81
|
+
if card_data[:delta]
|
82
|
+
div(class: "mt-auto pt-4 border-t border-gray-100") do
|
83
|
+
div(class: "flex items-center justify-between") do
|
84
|
+
span(class: delta_badge_classes(card_data)) do
|
85
|
+
trend_icon(card_data)
|
86
|
+
span(class: "ml-1 font-semibold") { "#{card_data[:delta].abs}%" }
|
87
|
+
end
|
88
|
+
span(class: "text-xs text-gray-500") do
|
89
|
+
"vs #{card_data[:compare_period] || 'last period'}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def render_chart_card(card, card_data)
|
98
|
+
div(class: "p-6 flex flex-col h-full") do
|
99
|
+
div(class: "flex-1") do
|
100
|
+
div(
|
101
|
+
class: "w-full bg-gray-50 rounded-lg flex items-center justify-center",
|
102
|
+
style: "height: #{chart_height(card)}px;"
|
103
|
+
) do
|
104
|
+
if card_data[:chart_data]
|
105
|
+
div(class: "text-gray-600") { "Chart: #{card[:title]}" }
|
106
|
+
else
|
107
|
+
div(class: "text-gray-400") { "No data available" }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def render_table_card(card, card_data)
|
115
|
+
div(class: "overflow-hidden") do
|
116
|
+
if card_data[:rows]&.any?
|
117
|
+
table(class: "min-w-full divide-y divide-gray-200") do
|
118
|
+
# Table header
|
119
|
+
if card_data[:headers]
|
120
|
+
thead(class: "bg-gray-50") do
|
121
|
+
tr do
|
122
|
+
card_data[:headers].each do |header|
|
123
|
+
th(class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider") do
|
124
|
+
header
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Table body
|
132
|
+
tbody(class: "bg-white divide-y divide-gray-200") do
|
133
|
+
card_data[:rows].each do |row|
|
134
|
+
tr do
|
135
|
+
row.each do |cell|
|
136
|
+
td(class: "px-6 py-4 whitespace-nowrap text-sm text-gray-900") do
|
137
|
+
cell
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
else
|
145
|
+
div(class: "p-6 text-center text-gray-400") do
|
146
|
+
"No data available"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def render_default_card(card, card_data)
|
153
|
+
div(class: "p-6") do
|
154
|
+
div(class: "text-gray-600") do
|
155
|
+
card[:title] || "Card content"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def render_sparkline(card, card_data)
|
161
|
+
svg(viewBox: "0 0 100 30", class: "w-full h-12") do
|
162
|
+
points = sparkline_points(card_data)
|
163
|
+
|
164
|
+
# Gradient definition
|
165
|
+
defs do
|
166
|
+
linearGradient(id: "sparkline-gradient-#{card[:name]}", x1: "0%", y1: "0%", x2: "0%", y2: "100%") do
|
167
|
+
stop(offset: "0%", style: "stop-color:#{sparkline_color(card_data)};stop-opacity:0.3")
|
168
|
+
stop(offset: "100%", style: "stop-color:#{sparkline_color(card_data)};stop-opacity:0")
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Line
|
173
|
+
polyline(
|
174
|
+
points: points,
|
175
|
+
stroke: sparkline_color(card_data),
|
176
|
+
stroke_width: "2",
|
177
|
+
fill: "none",
|
178
|
+
class: "drop-shadow-sm"
|
179
|
+
)
|
180
|
+
|
181
|
+
# Fill area
|
182
|
+
polygon(
|
183
|
+
points: "#{points},100,30 0,30",
|
184
|
+
fill: "url(#sparkline-gradient-#{card[:name]})"
|
185
|
+
)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def chart_height(card)
|
190
|
+
base_height = 200
|
191
|
+
card[:rows] > 1 ? base_height * card[:rows] : base_height
|
192
|
+
end
|
193
|
+
|
194
|
+
# Helper methods
|
195
|
+
def metric_value_classes(card_data)
|
196
|
+
base_classes = "text-3xl font-bold mb-1"
|
197
|
+
if card_data[:delta]
|
198
|
+
case trend_direction(card_data)
|
199
|
+
when :positive
|
200
|
+
"#{base_classes} text-green-600"
|
201
|
+
when :negative
|
202
|
+
"#{base_classes} text-red-600"
|
203
|
+
else
|
204
|
+
"#{base_classes} text-gray-900"
|
205
|
+
end
|
206
|
+
else
|
207
|
+
"#{base_classes} text-gray-900"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def trend_indicator_classes(card_data)
|
212
|
+
base_classes = "flex items-center justify-center w-10 h-10 rounded-full"
|
213
|
+
case trend_direction(card_data)
|
214
|
+
when :positive
|
215
|
+
"#{base_classes} bg-green-100 text-green-600"
|
216
|
+
when :negative
|
217
|
+
"#{base_classes} bg-red-100 text-red-600"
|
218
|
+
else
|
219
|
+
"#{base_classes} bg-gray-100 text-gray-600"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def delta_badge_classes(card_data)
|
224
|
+
base_classes = "inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
|
225
|
+
case trend_direction(card_data)
|
226
|
+
when :positive
|
227
|
+
"#{base_classes} bg-green-100 text-green-800"
|
228
|
+
when :negative
|
229
|
+
"#{base_classes} bg-red-100 text-red-800"
|
230
|
+
else
|
231
|
+
"#{base_classes} bg-gray-100 text-gray-600"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def trend_direction(card_data)
|
236
|
+
return :neutral unless card_data[:delta]
|
237
|
+
card_data[:delta].to_f > 0 ? :positive : (card_data[:delta].to_f < 0 ? :negative : :neutral)
|
238
|
+
end
|
239
|
+
|
240
|
+
def trend_icon(card_data)
|
241
|
+
case trend_direction(card_data)
|
242
|
+
when :positive
|
243
|
+
"↗"
|
244
|
+
when :negative
|
245
|
+
"↘"
|
246
|
+
else
|
247
|
+
"→"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def sparkline_color(card_data)
|
252
|
+
case trend_direction(card_data)
|
253
|
+
when :positive
|
254
|
+
"#059669" # green-600
|
255
|
+
when :negative
|
256
|
+
"#dc2626" # red-600
|
257
|
+
else
|
258
|
+
"#4f46e5" # indigo-600
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def sparkline_points(card_data)
|
263
|
+
return "" unless card_data[:sparkline_data]&.any?
|
264
|
+
|
265
|
+
data = card_data[:sparkline_data]
|
266
|
+
max_val = data.map { |d| d[:value] }.max
|
267
|
+
min_val = data.map { |d| d[:value] }.min
|
268
|
+
range = max_val - min_val
|
269
|
+
range = 1 if range == 0
|
270
|
+
|
271
|
+
data.map.with_index do |point, i|
|
272
|
+
x = i * (100.0 / (data.length - 1))
|
273
|
+
y = 25 - ((point[:value] - min_val) / range.to_f * 20)
|
274
|
+
"#{x},#{y}"
|
275
|
+
end.join(' ')
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
class ShowComponent < BaseComponent
|
4
|
+
def initialize(dashboard_class:, params: {}, request_path: nil)
|
5
|
+
@dashboard_class = dashboard_class
|
6
|
+
@params = params
|
7
|
+
@request_path = request_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
div(class: "p-3 sm:p-4 md:p-6 max-w-7xl mx-auto") do
|
12
|
+
render_dashboard_header
|
13
|
+
render_dashboard_filters
|
14
|
+
render_dashboard_grid
|
15
|
+
render_dashboard_footer
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def render_dashboard_header
|
22
|
+
div(class: "mb-4 sm:mb-6") do
|
23
|
+
div(class: "flex flex-col sm:flex-row sm:items-center sm:justify-between") do
|
24
|
+
div(class: "mb-4 sm:mb-0") do
|
25
|
+
h1(class: "text-xl sm:text-2xl md:text-3xl font-bold text-gray-900") { @dashboard_class.title }
|
26
|
+
p(class: "text-gray-600 text-sm md:text-base mt-1") { "Monitor your key metrics and insights" }
|
27
|
+
end
|
28
|
+
render_header_actions
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def render_header_actions
|
34
|
+
div(class: "flex items-center space-x-2 sm:space-x-3") do
|
35
|
+
# Back to Admin button
|
36
|
+
a(
|
37
|
+
href: EasyAdmin::Engine.routes.url_helpers.root_path,
|
38
|
+
class: "inline-flex items-center px-3 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors duration-200"
|
39
|
+
) do
|
40
|
+
unsafe_raw <<~SVG
|
41
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
42
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
43
|
+
</svg>
|
44
|
+
SVG
|
45
|
+
span(class: "hidden sm:inline") { "Back to Admin" }
|
46
|
+
span(class: "sm:hidden") { "Back" }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Refresh button
|
50
|
+
button(
|
51
|
+
type: "button",
|
52
|
+
class: "inline-flex items-center px-3 py-2 bg-blue-600 border border-transparent rounded-lg text-sm font-medium text-white hover:bg-blue-700 transition-colors duration-200",
|
53
|
+
data: { action: "click->dashboard#refresh" }
|
54
|
+
) do
|
55
|
+
unsafe_raw <<~SVG
|
56
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
57
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
58
|
+
</svg>
|
59
|
+
SVG
|
60
|
+
span(class: "hidden sm:inline") { "Refresh" }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def render_dashboard_filters
|
66
|
+
div(class: "bg-white rounded-xl shadow-sm border border-gray-200 mb-4 sm:mb-6") do
|
67
|
+
div(class: "px-4 sm:px-6 py-3 sm:py-4 border-b border-gray-100") do
|
68
|
+
h3(class: "text-sm font-semibold text-gray-900") { "Filters" }
|
69
|
+
end
|
70
|
+
div(class: "p-4 sm:p-6") do
|
71
|
+
render_filters_form
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_filters_form
|
77
|
+
form(action: @request_path, method: :get, class: "space-y-4") do
|
78
|
+
div(class: "grid grid-cols-1 md:grid-cols-3 gap-4") do
|
79
|
+
# Date Range
|
80
|
+
div(class: "md:col-span-2") do
|
81
|
+
label(class: "block text-sm font-medium text-gray-700 mb-2") { "Date Range" }
|
82
|
+
div(class: "flex flex-col sm:flex-row sm:items-center gap-2") do
|
83
|
+
render EasyAdmin::DatePickerComponent.new(
|
84
|
+
name: :date_from,
|
85
|
+
value: @params[:date_from],
|
86
|
+
placeholder: "Start date"
|
87
|
+
)
|
88
|
+
span(class: "text-gray-400 text-sm hidden sm:block") { "—" }
|
89
|
+
render EasyAdmin::DatePickerComponent.new(
|
90
|
+
name: :date_to,
|
91
|
+
value: @params[:date_to],
|
92
|
+
placeholder: "End date"
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Apply Button
|
98
|
+
div(class: "flex items-end") do
|
99
|
+
input(
|
100
|
+
type: "submit",
|
101
|
+
value: "Apply Filters",
|
102
|
+
class: "w-full px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors duration-200 cursor-pointer"
|
103
|
+
)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
render_quick_ranges
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def render_quick_ranges
|
112
|
+
div do
|
113
|
+
label(class: "block text-sm font-medium text-gray-700 mb-2") { "Quick Filters" }
|
114
|
+
div(class: "flex flex-wrap gap-2") do
|
115
|
+
render_quick_range_link("Today", "today")
|
116
|
+
render_quick_range_link("Last 7 Days", "7d")
|
117
|
+
render_quick_range_link("Last 30 Days", "30d")
|
118
|
+
render_quick_range_link("Last 90 Days", "90d")
|
119
|
+
render_quick_range_link("Last Year", "1y")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def render_quick_range_link(label, period)
|
125
|
+
is_active = @params[:period] == period
|
126
|
+
link_classes = if is_active
|
127
|
+
"inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 bg-blue-600 text-white shadow-sm"
|
128
|
+
else
|
129
|
+
"inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 bg-white text-gray-700 border border-gray-300 hover:bg-gray-50"
|
130
|
+
end
|
131
|
+
|
132
|
+
a(
|
133
|
+
href: "#{@request_path}?period=#{period}",
|
134
|
+
class: link_classes
|
135
|
+
) { label }
|
136
|
+
end
|
137
|
+
|
138
|
+
def render_dashboard_grid
|
139
|
+
div(
|
140
|
+
class: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6",
|
141
|
+
data: { controller: "dashboard" }
|
142
|
+
) do
|
143
|
+
@dashboard_class.visible_cards.each do |card|
|
144
|
+
render render_card(card, @dashboard_class, @params)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def render_dashboard_footer
|
150
|
+
div(class: "mt-8 flex flex-col sm:flex-row sm:items-center sm:justify-between text-sm text-gray-500 border-t border-gray-200 pt-6") do
|
151
|
+
div(class: "flex items-center space-x-4") do
|
152
|
+
span { "Last updated #{time_ago_in_words(Time.current)} ago" }
|
153
|
+
div(class: "w-2 h-2 bg-green-400 rounded-full")
|
154
|
+
span(class: "text-green-600 font-medium") { "Live" }
|
155
|
+
end
|
156
|
+
div(class: "mt-4 sm:mt-0") do
|
157
|
+
a(href: "#", class: "text-blue-600 hover:text-blue-700 font-medium") { "View All Reports" }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|