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,232 @@
1
+ module EasyAdmin
2
+ class CombinedFiltersComponent < Phlex::HTML
3
+ def initialize(resource_class:, current_params: {}, search_params: {}, current_period: nil)
4
+ @resource_class = resource_class
5
+ @current_params = current_params
6
+ @search_params = search_params || {}
7
+ @current_period = current_period
8
+ end
9
+
10
+ def view_template
11
+ return unless has_any_filters?
12
+
13
+ div(
14
+ id: "combined-filters",
15
+ class: "bg-white shadow rounded-lg mb-6",
16
+ data: { controller: "collapsible-filters" }
17
+ ) do
18
+ # Header with collapse toggle
19
+ div(
20
+ class: "px-6 py-4 border-b border-gray-200 cursor-pointer hover:bg-gray-50 transition-colors duration-150",
21
+ data: { action: "click->collapsible-filters#toggle" }
22
+ ) do
23
+ div(class: "flex items-center justify-between") do
24
+ div(class: "flex items-center space-x-2") do
25
+ h3(class: "text-lg font-medium text-gray-900") { "Filters" }
26
+ if has_active_filters?
27
+ span(class: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800") do
28
+ active_filter_count.to_s
29
+ end
30
+ end
31
+ end
32
+
33
+ # Collapse/Expand icon
34
+ div(data: { collapsible_filters_target: "icon" }) do
35
+ unsafe_raw '<svg class="w-5 h-5 text-gray-400 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
37
+ </svg>'
38
+ end
39
+ end
40
+ end
41
+
42
+ # Collapsible content
43
+ div(
44
+ class: "transition-all duration-300 ease-in-out",
45
+ data: { collapsible_filters_target: "content" }
46
+ ) do
47
+ form(
48
+ method: "get",
49
+ action: route_helper.resources_path(@resource_class.route_key),
50
+ class: "space-y-4"
51
+ ) do
52
+ # Preserve existing params (except filter params)
53
+ preserve_params
54
+
55
+ # Field Filters (Advanced)
56
+ if @resource_class.filterable_fields.any?
57
+ div(class: "px-6 pt-4") do
58
+ label(class: "block text-sm font-medium text-gray-700 mb-3") { "Advanced Filters" }
59
+ div(class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4") do
60
+ @resource_class.filterable_fields.each do |field|
61
+ render_filter_field(field)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # Quick Period Filters
68
+ if has_quick_filters?
69
+ div(class: "px-6 #{@resource_class.filterable_fields.any? ? 'pb-4' : 'py-4'}") do
70
+ div(class: "mb-3") do
71
+ label(class: "block text-sm font-medium text-gray-700 mb-2") { "Quick Filters" }
72
+ render_quick_filters
73
+ end
74
+ end
75
+ end
76
+
77
+ # Action buttons
78
+ div(class: "px-6 py-4 bg-gray-50 border-t border-gray-200") do
79
+ div(class: "flex items-center justify-between") do
80
+ # Clear filters button
81
+ if has_active_filters?
82
+ a(
83
+ href: build_clear_filters_url,
84
+ class: "text-sm text-red-600 hover:text-red-800 font-medium"
85
+ ) { "Clear all filters" }
86
+ else
87
+ div # Empty div for spacing
88
+ end
89
+
90
+ # Apply button
91
+ button(
92
+ type: "submit",
93
+ class: "inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
94
+ ) do
95
+ unsafe_raw '<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
96
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707v6.586l-4-4V9.414a1 1 0 00-.293-.707L3.293 6.293A1 1 0 013 5.586V4z"></path>
97
+ </svg>'
98
+ span { "Apply Filters" }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def has_any_filters?
110
+ has_quick_filters? || @resource_class.filterable_fields.any?
111
+ end
112
+
113
+ def has_quick_filters?
114
+ true # We always show period filters
115
+ end
116
+
117
+ def route_helper
118
+ @route_helper ||= EasyAdmin::Engine.routes.url_helpers
119
+ end
120
+
121
+ def preserve_params
122
+ # Preserve non-filter params (scope, sort, direction, page, period)
123
+ preserved_params = @current_params.except(:q)
124
+ preserved_params.each do |key, value|
125
+ input(type: "hidden", name: key, value: value)
126
+ end
127
+ end
128
+
129
+ def render_quick_filters
130
+ div(class: "flex flex-wrap gap-2") do
131
+ render_quick_filter_link("All Time", nil)
132
+ render_quick_filter_link("Today", "today")
133
+ render_quick_filter_link("Last 7 Days", "7d")
134
+ render_quick_filter_link("Last 30 Days", "30d")
135
+ render_quick_filter_link("Last 90 Days", "90d")
136
+ render_quick_filter_link("Last Year", "1y")
137
+ end
138
+ end
139
+
140
+ def render_quick_filter_link(label, period)
141
+ # Check if active - nil period means "All Time" is active when @current_period is blank
142
+ active = if period.nil?
143
+ @current_period.blank?
144
+ else
145
+ @current_period.to_s == period.to_s
146
+ end
147
+ css_classes = if active
148
+ "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"
149
+ else
150
+ "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"
151
+ end
152
+
153
+ a(href: build_quick_filter_url(period), class: css_classes, data: { turbo_prefetch: "false" }) { label }
154
+ end
155
+
156
+
157
+ def build_quick_filter_url(period)
158
+ current_params = @current_params.dup
159
+
160
+ # Set or remove period parameter
161
+ if period
162
+ current_params[:period] = period
163
+ else
164
+ current_params.delete(:period)
165
+ end
166
+
167
+ base_url = route_helper.resources_path(@resource_class.route_key)
168
+ query_string = current_params.to_query
169
+ query_string.present? ? "#{base_url}?#{query_string}" : base_url
170
+ end
171
+
172
+ def render_filter_field(field)
173
+ component_class = filter_component_for_field(field)
174
+ render component_class.new(field: field, search_params: @search_params)
175
+ end
176
+
177
+ def filter_component_for_field(field)
178
+ case field[:type]
179
+ when :string, :email, :text
180
+ EasyAdmin::Fields::Index::Filters::StringComponent
181
+ when :boolean
182
+ EasyAdmin::Fields::Index::Filters::BooleanComponent
183
+ when :select, :belongs_to
184
+ EasyAdmin::Fields::Index::Filters::SelectComponent
185
+ when :number, :integer
186
+ EasyAdmin::Fields::Index::Filters::NumberComponent
187
+ when :date, :datetime
188
+ EasyAdmin::Fields::Index::Filters::DateComponent
189
+ else
190
+ EasyAdmin::Fields::Index::Filters::StringComponent
191
+ end
192
+ end
193
+
194
+ def has_active_filters?
195
+ has_active_search_params? || @current_period.present?
196
+ end
197
+
198
+ def has_active_search_params?
199
+ return false unless @search_params.present?
200
+
201
+ @search_params.each do |key, value|
202
+ return true if value.present?
203
+ end
204
+
205
+ false
206
+ end
207
+
208
+ def active_filter_count
209
+ count = 0
210
+ count += 1 if @current_period.present?
211
+
212
+ if @search_params.present?
213
+ @search_params.each do |key, value|
214
+ count += 1 if value.present?
215
+ end
216
+ end
217
+
218
+ count
219
+ end
220
+
221
+ def build_clear_filters_url
222
+ base_url = route_helper.resources_path(@resource_class.route_key)
223
+ clear_params = @current_params.except(:q, :period)
224
+
225
+ if clear_params.present? && !clear_params.empty?
226
+ "#{base_url}?#{clear_params.to_query}"
227
+ else
228
+ base_url
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,61 @@
1
+ module EasyAdmin
2
+ class ConfirmationModalComponent < BaseComponent
3
+ def initialize(title:, message:, confirm_text: "Confirm", cancel_text: "Cancel", danger: false)
4
+ @title = title
5
+ @message = message
6
+ @confirm_text = confirm_text
7
+ @cancel_text = cancel_text
8
+ @danger = danger
9
+ end
10
+
11
+ def view_template
12
+ div(
13
+ id: "confirmation-modal",
14
+ class: "fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50",
15
+ data: {
16
+ controller: "confirmation-modal",
17
+ action: "click->confirmation-modal#clickOutside"
18
+ }
19
+ ) do
20
+ div(
21
+ class: "relative top-20 mx-auto p-5 border w-11/12 max-w-md shadow-lg rounded-md bg-white",
22
+ data: { confirmation_modal_target: "modal" }
23
+ ) do
24
+ # Header
25
+ div(class: "flex items-center justify-between pb-4 mb-4 border-b border-gray-200") do
26
+ h3(class: "text-lg font-semibold text-gray-900") { @title }
27
+ button(
28
+ type: "button",
29
+ class: "text-gray-400 hover:text-gray-600 text-xl",
30
+ data: { action: "click->confirmation-modal#cancel" }
31
+ ) { "×" }
32
+ end
33
+
34
+ # Message
35
+ p(class: "text-sm text-gray-600 mb-4") { @message }
36
+
37
+ # Action buttons
38
+ div(class: "flex justify-end space-x-3 pt-4 border-t border-gray-200 mt-4") do
39
+ button(
40
+ type: "button",
41
+ class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50",
42
+ data: { action: "click->confirmation-modal#cancel" }
43
+ ) { @cancel_text }
44
+
45
+ confirm_button_class = if @danger
46
+ "px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700"
47
+ else
48
+ "px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
49
+ end
50
+
51
+ button(
52
+ type: "button",
53
+ class: confirm_button_class,
54
+ data: { action: "click->confirmation-modal#confirm" }
55
+ ) { @confirm_text }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,161 @@
1
+ module EasyAdmin
2
+ class ContextMenuComponent < BaseComponent
3
+ def initialize(record:, resource_class:, actions:)
4
+ @record = record
5
+ @resource_class = resource_class
6
+ @actions = actions
7
+ end
8
+
9
+ def view_template
10
+ div(class: "context-menu bg-white rounded-lg shadow-lg border border-gray-200 py-1 min-w-48") do
11
+ # Default actions
12
+ render_default_actions
13
+
14
+ # Custom row actions
15
+ if @actions.any?
16
+ render_divider
17
+ render_custom_actions
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def render_default_actions
25
+ # View action
26
+ a(
27
+ href: resource_show_url,
28
+ class: "context-menu-item w-full text-left block",
29
+ data: { turbo: "false" }
30
+ ) do
31
+ div(class: "flex items-center space-x-2") do
32
+ unsafe_raw <<~SVG
33
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
35
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
36
+ </svg>
37
+ SVG
38
+ span { "View" }
39
+ end
40
+ end
41
+
42
+ # Edit action
43
+ a(
44
+ href: resource_edit_url,
45
+ class: "context-menu-item w-full text-left block",
46
+ data: { turbo: "false" }
47
+ ) do
48
+ div(class: "flex items-center space-x-2") do
49
+ unsafe_raw <<~SVG
50
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
51
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
52
+ </svg>
53
+ SVG
54
+ span { "Edit" }
55
+ end
56
+ end
57
+
58
+ # Delete action
59
+ button(
60
+ type: "button",
61
+ class: "context-menu-item danger w-full text-left",
62
+ data: {
63
+ controller: "row-action",
64
+ action: "click->row-action#execute",
65
+ row_action_action_class_value: "EasyAdmin::DeleteAction",
66
+ row_action_execution_mode_value: "instant",
67
+ row_action_confirm_value: "Are you sure you want to delete this #{@resource_class.singular_title.downcase}?",
68
+ row_action_record_id_value: @record.id,
69
+ row_action_resource_name_value: @resource_class.route_key
70
+ }
71
+ ) do
72
+ div(class: "flex items-center space-x-2") do
73
+ unsafe_raw <<~SVG
74
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
76
+ </svg>
77
+ SVG
78
+ span { "Delete" }
79
+ end
80
+ end
81
+ end
82
+
83
+ def render_custom_actions
84
+ @actions.each do |action_data|
85
+ action_instance = action_data[:instance]
86
+ action_class = action_data[:class]
87
+
88
+ button(
89
+ type: "button",
90
+ class: "context-menu-item w-full text-left #{action_style_class(action_instance)}",
91
+ data: {
92
+ controller: "row-action",
93
+ action: "click->row-action#execute",
94
+ row_action_action_class_value: action_class.name,
95
+ row_action_execution_mode_value: action_instance.class.execution_mode.to_s,
96
+ row_action_confirm_value: action_instance.class.confirm_message,
97
+ row_action_record_id_value: @record.id,
98
+ row_action_resource_name_value: @resource_class.route_key
99
+ }
100
+ ) do
101
+ div(class: "flex items-center space-x-2") do
102
+ if action_instance.class.icon
103
+ span(class: "text-base") { action_instance.class.icon }
104
+ else
105
+ render_default_icon(action_instance)
106
+ end
107
+ span { action_instance.class.label }
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def render_divider
114
+ div(class: "context-menu-divider")
115
+ end
116
+
117
+ def action_style_class(action_instance)
118
+ case action_instance.class.style
119
+ when :danger
120
+ "danger"
121
+ when :warning
122
+ "text-yellow-600 hover:bg-yellow-50 hover:text-yellow-700"
123
+ when :success
124
+ "text-green-600 hover:bg-green-50 hover:text-green-700"
125
+ else
126
+ ""
127
+ end
128
+ end
129
+
130
+ def render_default_icon(action_instance)
131
+ case action_instance.class.style
132
+ when :danger
133
+ unsafe_raw <<~SVG
134
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
135
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
136
+ </svg>
137
+ SVG
138
+ when :success
139
+ unsafe_raw <<~SVG
140
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
141
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
142
+ </svg>
143
+ SVG
144
+ else
145
+ unsafe_raw <<~SVG
146
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
147
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
148
+ </svg>
149
+ SVG
150
+ end
151
+ end
152
+
153
+ def resource_show_url
154
+ easy_admin_url_helpers.resource_path(@resource_class.route_key, @record)
155
+ end
156
+
157
+ def resource_edit_url
158
+ easy_admin_url_helpers.edit_resource_path(@resource_class.route_key, @record)
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,152 @@
1
+ module EasyAdmin
2
+ module Dashboards
3
+ class BaseCardComponent < EasyAdmin::BaseComponent
4
+ attr_reader :card, :dashboard_class, :data, :request_params
5
+
6
+ def initialize(card:, dashboard_class:, data: nil, request_params: nil)
7
+ @card = card
8
+ @dashboard_class = dashboard_class
9
+ @data = data
10
+ @request_params = request_params || ActionController::Parameters.new.permit!
11
+ end
12
+
13
+ def view_template
14
+ div(
15
+ class: card_classes,
16
+ role: "region",
17
+ "aria-labelledby": card_title_id
18
+ ) do
19
+ render_card_header
20
+ render_card_body
21
+ render_card_footer if card[:footer]
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def card_classes
28
+ classes = ["bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"]
29
+ classes << "flex flex-col" # Ensure flex layout
30
+ classes << column_span_class
31
+ classes << row_span_class if card[:rows] > 1
32
+ classes.join(" ")
33
+ end
34
+
35
+ def column_span_class
36
+ cols = card[:cols] || 1
37
+ case cols
38
+ when 1
39
+ "col-span-1"
40
+ when 2
41
+ "col-span-1 sm:col-span-2"
42
+ when 3
43
+ "col-span-1 sm:col-span-2 lg:col-span-3"
44
+ when 4
45
+ "col-span-1 sm:col-span-2 lg:col-span-4"
46
+ else
47
+ "col-span-1"
48
+ end
49
+ end
50
+
51
+ def row_span_class
52
+ "row-span-#{card[:rows]}"
53
+ end
54
+
55
+ def card_title_id
56
+ "#{card[:name]}_title"
57
+ end
58
+
59
+ def render_card_header
60
+ div(class: "px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex items-center justify-between") do
61
+ h2(id: card_title_id, class: "text-lg font-semibold text-gray-900") { plain card[:title] }
62
+ div(class: "flex items-center space-x-2") do
63
+ render_card_actions if card[:actions]
64
+ end
65
+ end
66
+ end
67
+
68
+ def render_card_body
69
+ body_classes = card[:type] == :table ? "" : "p-6"
70
+ div(class: body_classes) do
71
+ turbo_frame(
72
+ id: "card_#{card[:name]}",
73
+ src: card_url,
74
+ loading: "lazy",
75
+ data: { controller: "dashboard-card" }
76
+ ) do
77
+ render_skeleton
78
+ end
79
+ end
80
+ end
81
+
82
+ def render_card_footer
83
+ div(class: "px-6 py-4 bg-gray-50/30 border-t border-gray-100") do
84
+ plain card[:footer]
85
+ end
86
+ end
87
+
88
+ def render_card_actions
89
+ # Override in subclasses
90
+ end
91
+
92
+ def render_skeleton
93
+ case card[:type]
94
+ when :metric
95
+ render_metric_skeleton
96
+ when :chart
97
+ render_chart_skeleton
98
+ when :table
99
+ render_table_skeleton
100
+ else
101
+ render_default_skeleton
102
+ end
103
+ end
104
+
105
+ def render_metric_skeleton
106
+ div(class: "animate-pulse") do
107
+ div(class: "h-8 bg-gray-200 rounded mb-2")
108
+ div(class: "h-4 bg-gray-200 rounded w-3/5")
109
+ end
110
+ end
111
+
112
+ def render_chart_skeleton
113
+ div(class: "animate-pulse") do
114
+ div(class: "h-32 bg-gray-200 rounded")
115
+ end
116
+ end
117
+
118
+ def render_table_skeleton
119
+ div(class: "animate-pulse space-y-3") do
120
+ div(class: "grid grid-cols-4 gap-4 mb-4") do
121
+ 4.times do
122
+ div(class: "h-4 bg-gray-200 rounded")
123
+ end
124
+ end
125
+ 5.times do
126
+ div(class: "grid grid-cols-4 gap-4") do
127
+ 4.times do
128
+ div(class: "h-4 bg-gray-100 rounded")
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def render_default_skeleton
136
+ div(class: "animate-pulse space-y-3") do
137
+ div(class: "h-6 bg-gray-200 rounded")
138
+ div(class: "h-4 bg-gray-200 rounded")
139
+ div(class: "h-4 bg-gray-200 rounded w-4/5")
140
+ end
141
+ end
142
+
143
+ def card_url
144
+ easy_admin_url_helpers.dashboards_dashboard_card_path(
145
+ dashboard_name: dashboard_class.route_key,
146
+ card_name: card[:name],
147
+ **request_params.to_h.symbolize_keys
148
+ )
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,23 @@
1
+ module EasyAdmin
2
+ module Dashboards
3
+ class CardErrorComponent < BaseComponent
4
+ def initialize(error:)
5
+ @error = error
6
+ end
7
+
8
+ def view_template
9
+ div(class: "p-6 text-center") do
10
+ div(class: "mb-4") do
11
+ unsafe_raw <<~SVG
12
+ <svg class="mx-auto h-12 w-12 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
13
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
14
+ </svg>
15
+ SVG
16
+ end
17
+ div(class: "text-red-600 font-medium mb-2") { "Error loading card" }
18
+ div(class: "text-sm text-gray-500") { @error }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end