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.
Files changed (203) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/easy_admin.base.js +43505 -0
  6. data/app/assets/builds/easy_admin.base.js.map +7 -0
  7. data/app/assets/builds/easy_admin.css +6141 -0
  8. data/app/assets/config/easy_admin_manifest.js +1 -0
  9. data/app/assets/images/jsoneditor-icons.svg +749 -0
  10. data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
  11. data/app/components/easy_admin/base_component.rb +35 -0
  12. data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
  13. data/app/components/easy_admin/batch_action_form_component.rb +124 -0
  14. data/app/components/easy_admin/combined_filters_component.rb +232 -0
  15. data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
  16. data/app/components/easy_admin/context_menu_component.rb +161 -0
  17. data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
  18. data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
  19. data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
  20. data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
  21. data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
  22. data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
  23. data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
  24. data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
  25. data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
  26. data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
  27. data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
  28. data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
  29. data/app/components/easy_admin/dashboards/show_component.rb +163 -0
  30. data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
  31. data/app/components/easy_admin/date_picker_component.rb +188 -0
  32. data/app/components/easy_admin/fields/base_component.rb +101 -0
  33. data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
  34. data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
  35. data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
  36. data/app/components/easy_admin/fields/form/date_component.rb +55 -0
  37. data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
  38. data/app/components/easy_admin/fields/form/email_component.rb +55 -0
  39. data/app/components/easy_admin/fields/form/file_component.rb +190 -0
  40. data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
  41. data/app/components/easy_admin/fields/form/json_component.rb +81 -0
  42. data/app/components/easy_admin/fields/form/number_component.rb +55 -0
  43. data/app/components/easy_admin/fields/form/select_component.rb +326 -0
  44. data/app/components/easy_admin/fields/form/text_component.rb +55 -0
  45. data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
  46. data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
  47. data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
  48. data/app/components/easy_admin/fields/index/date_component.rb +13 -0
  49. data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
  50. data/app/components/easy_admin/fields/index/email_component.rb +24 -0
  51. data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
  52. data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
  53. data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
  54. data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
  55. data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
  56. data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
  57. data/app/components/easy_admin/fields/index/json_component.rb +23 -0
  58. data/app/components/easy_admin/fields/index/number_component.rb +20 -0
  59. data/app/components/easy_admin/fields/index/select_component.rb +25 -0
  60. data/app/components/easy_admin/fields/index/text_component.rb +20 -0
  61. data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
  62. data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
  63. data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
  64. data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
  65. data/app/components/easy_admin/fields/show/date_component.rb +13 -0
  66. data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
  67. data/app/components/easy_admin/fields/show/email_component.rb +19 -0
  68. data/app/components/easy_admin/fields/show/file_component.rb +304 -0
  69. data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
  70. data/app/components/easy_admin/fields/show/json_component.rb +45 -0
  71. data/app/components/easy_admin/fields/show/number_component.rb +20 -0
  72. data/app/components/easy_admin/fields/show/select_component.rb +25 -0
  73. data/app/components/easy_admin/fields/show/text_component.rb +17 -0
  74. data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
  75. data/app/components/easy_admin/filters_component.rb +120 -0
  76. data/app/components/easy_admin/form_tabs_component.rb +166 -0
  77. data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
  78. data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
  79. data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
  80. data/app/components/easy_admin/modal_frame_component.rb +26 -0
  81. data/app/components/easy_admin/navbar_component.rb +226 -0
  82. data/app/components/easy_admin/notification_component.rb +83 -0
  83. data/app/components/easy_admin/pagination_component.rb +188 -0
  84. data/app/components/easy_admin/quick_filters_component.rb +65 -0
  85. data/app/components/easy_admin/resource_pagination_component.rb +14 -0
  86. data/app/components/easy_admin/resources/index_component.rb +211 -0
  87. data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
  88. data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
  89. data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
  90. data/app/components/easy_admin/resources/table_component.rb +206 -0
  91. data/app/components/easy_admin/resources/table_row_component.rb +160 -0
  92. data/app/components/easy_admin/row_action_form_component.rb +127 -0
  93. data/app/components/easy_admin/scopes_component.rb +224 -0
  94. data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
  95. data/app/components/easy_admin/show_layout_component.rb +600 -0
  96. data/app/components/easy_admin/sidebar_component.rb +174 -0
  97. data/app/components/easy_admin/turbo/response_component.rb +40 -0
  98. data/app/components/easy_admin/turbo/stream_component.rb +28 -0
  99. data/app/controllers/easy_admin/application_controller.rb +66 -0
  100. data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
  101. data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
  102. data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
  103. data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
  104. data/app/controllers/easy_admin/passwords_controller.rb +15 -0
  105. data/app/controllers/easy_admin/registrations_controller.rb +52 -0
  106. data/app/controllers/easy_admin/resources_controller.rb +907 -0
  107. data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
  108. data/app/controllers/easy_admin/sessions_controller.rb +32 -0
  109. data/app/controllers/easy_admin/settings_controller.rb +94 -0
  110. data/app/helpers/easy_admin/application_helper.rb +4 -0
  111. data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
  112. data/app/helpers/easy_admin/fields_helper.rb +27 -0
  113. data/app/helpers/easy_admin/pagy_helper.rb +30 -0
  114. data/app/helpers/easy_admin/resources_helper.rb +39 -0
  115. data/app/javascript/easy_admin/application.js +12 -0
  116. data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
  117. data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
  118. data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
  119. data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
  120. data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
  121. data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
  122. data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
  123. data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
  124. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
  125. data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
  126. data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
  127. data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
  128. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
  129. data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
  130. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
  131. data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
  132. data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
  133. data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
  134. data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
  135. data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
  136. data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
  137. data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
  138. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
  139. data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
  140. data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
  141. data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
  142. data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
  143. data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
  144. data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
  145. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
  146. data/app/javascript/easy_admin/controllers.js +54 -0
  147. data/app/javascript/easy_admin.base.js +4 -0
  148. data/app/models/easy_admin/admin_user.rb +53 -0
  149. data/app/models/easy_admin/application_record.rb +5 -0
  150. data/app/views/easy_admin/dashboard/index.html.erb +3 -0
  151. data/app/views/easy_admin/dashboards/show.html.erb +7 -0
  152. data/app/views/easy_admin/passwords/edit.html.erb +42 -0
  153. data/app/views/easy_admin/passwords/new.html.erb +41 -0
  154. data/app/views/easy_admin/registrations/new.html.erb +65 -0
  155. data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
  156. data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
  157. data/app/views/easy_admin/resources/edit.html.erb +151 -0
  158. data/app/views/easy_admin/resources/index.html.erb +12 -0
  159. data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
  160. data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
  161. data/app/views/easy_admin/resources/new.html.erb +100 -0
  162. data/app/views/easy_admin/resources/show.html.erb +31 -0
  163. data/app/views/easy_admin/sessions/new.html.erb +55 -0
  164. data/app/views/easy_admin/settings/_form.html.erb +51 -0
  165. data/app/views/easy_admin/settings/index.html.erb +53 -0
  166. data/app/views/layouts/easy_admin/application.html.erb +48 -0
  167. data/app/views/layouts/easy_admin/auth.html.erb +34 -0
  168. data/config/initializers/easy_admin_card_factory.rb +27 -0
  169. data/config/initializers/pagy.rb +15 -0
  170. data/config/initializers/rack_mini_profiler.rb +67 -0
  171. data/config/routes.rb +70 -0
  172. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
  173. data/lib/easy-admin.rb +32 -0
  174. data/lib/easy_admin/action.rb +159 -0
  175. data/lib/easy_admin/batch_action.rb +134 -0
  176. data/lib/easy_admin/configuration.rb +75 -0
  177. data/lib/easy_admin/dashboard.rb +110 -0
  178. data/lib/easy_admin/dashboard_registry.rb +30 -0
  179. data/lib/easy_admin/delete_action.rb +22 -0
  180. data/lib/easy_admin/engine.rb +54 -0
  181. data/lib/easy_admin/field.rb +118 -0
  182. data/lib/easy_admin/resource.rb +806 -0
  183. data/lib/easy_admin/resource_registry.rb +22 -0
  184. data/lib/easy_admin/types/json_type.rb +25 -0
  185. data/lib/easy_admin/version.rb +3 -0
  186. data/lib/generators/easy_admin/auth_generator.rb +69 -0
  187. data/lib/generators/easy_admin/card/card_generator.rb +94 -0
  188. data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
  189. data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
  190. data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
  191. data/lib/generators/easy_admin/install_generator.rb +25 -0
  192. data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
  193. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
  194. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
  195. data/lib/generators/easy_admin/resource_generator.rb +43 -0
  196. data/lib/generators/easy_admin/templates/AUTH_README +35 -0
  197. data/lib/generators/easy_admin/templates/README +27 -0
  198. data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
  199. data/lib/generators/easy_admin/templates/devise.rb +267 -0
  200. data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
  201. data/lib/generators/easy_admin/templates/resource.rb +29 -0
  202. data/lib/tasks/easy_admin_tasks.rake +4 -0
  203. 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