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,82 @@
1
+ module EasyAdmin
2
+ class InfiniteScrollComponent < Phlex::HTML
3
+ def initialize(pagy:, resource_class:, current_params: {}, current_path:)
4
+ @pagy = pagy
5
+ @resource_class = resource_class
6
+ @current_params = current_params
7
+ @current_path = current_path
8
+ end
9
+
10
+ def view_template
11
+ div(
12
+ id: "infinite-scroll-container",
13
+ class: "infinite-scroll-container mt-6",
14
+ data: {
15
+ controller: "infinite-scroll",
16
+ infinite_scroll_url_value: next_page_url,
17
+ infinite_scroll_has_more_value: has_more_pages?
18
+ }
19
+ ) do
20
+ div(
21
+ class: "infinite-scroll-loading bg-gray-50 border border-gray-200 rounded-lg p-6 text-center hidden",
22
+ data: { infinite_scroll_target: "loading" }
23
+ ) do
24
+ div(class: "loading-spinner mb-3") do
25
+ div(class: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto")
26
+ end
27
+ div(class: "text-gray-600 text-sm font-medium mb-1") { "Loading more #{@resource_class.title.downcase}..." }
28
+ div(class: "text-gray-500 text-xs") { "Scroll down or click 'Load More' to see additional results" }
29
+ end
30
+
31
+ # "Load More" button - simple and clean
32
+ if has_more_pages?
33
+ div(
34
+ class: "infinite-scroll-load-more mt-6 text-center",
35
+ data: { infinite_scroll_target: "loadMore" }
36
+ ) do
37
+ a(
38
+ href: next_page_url,
39
+ class: "inline-flex items-center justify-center px-8 py-3 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 transition-colors duration-200",
40
+ data: { action: "click->infinite-scroll#loadMore" }
41
+ ) do
42
+ span { "Load More" }
43
+ end
44
+ end
45
+ end
46
+
47
+ # End message when no more pages - always show for infinite scroll
48
+ unless has_more_pages?
49
+ div(
50
+ class: "infinite-scroll-end mt-6 text-center bg-gray-50 border border-gray-200 rounded-lg p-6",
51
+ data: { infinite_scroll_target: "end" }
52
+ ) do
53
+ div(class: "mb-3") do
54
+ div(class: "w-12 h-12 mx-auto bg-gray-200 rounded-full flex items-center justify-center mb-3") do
55
+ unsafe_raw '<svg class="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
57
+ </svg>'
58
+ end
59
+ end
60
+ div(class: "text-gray-700 font-medium mb-1") { "All records loaded" }
61
+ div(class: "text-gray-400 text-xs") { "You've reached the end of the list" }
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def has_more_pages?
70
+ @pagy.next.present?
71
+ end
72
+
73
+ def next_page_url
74
+ return "" unless has_more_pages?
75
+
76
+ # Build URL with current params plus next page
77
+ # Use pagy.next which gives us the correct next page number
78
+ url_params = @current_params.merge(page: @pagy.next)
79
+ "#{@current_path}?#{url_params.to_query}"
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,128 @@
1
+ module EasyAdmin
2
+ class LazyChartCardComponent < Phlex::HTML
3
+ def initialize(chart_config:, record:)
4
+ @chart_config = chart_config
5
+ @record = record
6
+ end
7
+
8
+ def view_template
9
+ div(class: @chart_config[:css_classes]) do
10
+ if @chart_config[:title]
11
+ div(class: "card-header") do
12
+ h5(class: "card-title") { @chart_config[:title] }
13
+ end
14
+ end
15
+
16
+ div(class: "card-body") do
17
+ div(
18
+ id: chart_id,
19
+ data: {
20
+ controller: "chart",
21
+ chart_type_value: @chart_config[:chart_type],
22
+ chart_height_value: @chart_config[:height],
23
+ chart_config_value: chart_data.to_json,
24
+ chart_data_source_value: @chart_config[:data_source]
25
+ }
26
+ )
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def chart_id
34
+ "chart-#{@chart_config.object_id}"
35
+ end
36
+
37
+ def chart_data
38
+ base_config = @chart_config[:config] || {}
39
+
40
+ # If data_source is specified, fetch data dynamically
41
+ if @chart_config[:data_source]
42
+ dynamic_data = fetch_chart_data(@chart_config[:data_source])
43
+ base_config.merge(dynamic_data)
44
+ else
45
+ base_config
46
+ end
47
+ end
48
+
49
+ def fetch_chart_data(data_source)
50
+ case data_source
51
+ when Symbol
52
+ # Call method on record
53
+ @record.public_send(data_source) if @record.respond_to?(data_source)
54
+ when Proc
55
+ # Execute proc with record context
56
+ data_source.call(@record)
57
+ when String
58
+ # Treat as association or method name
59
+ if @record.respond_to?(data_source)
60
+ process_association_data(@record.public_send(data_source))
61
+ end
62
+ else
63
+ {}
64
+ end || {}
65
+ end
66
+
67
+ def process_association_data(data)
68
+ case data
69
+ when ActiveRecord::Relation, Array
70
+ # Process collection data for charts
71
+ case @chart_config[:chart_type]
72
+ when :pie, :donut
73
+ process_pie_data(data)
74
+ when :line, :area
75
+ process_time_series_data(data)
76
+ when :bar, :column
77
+ process_category_data(data)
78
+ else
79
+ { series: data.count }
80
+ end
81
+ else
82
+ { series: [data.to_i] }
83
+ end
84
+ end
85
+
86
+ def process_pie_data(data)
87
+ if data.respond_to?(:group)
88
+ grouped = data.group(:status).count # Example grouping
89
+ {
90
+ series: grouped.values,
91
+ labels: grouped.keys.map(&:humanize)
92
+ }
93
+ else
94
+ { series: [data.count], labels: ["Total"] }
95
+ end
96
+ end
97
+
98
+ def process_time_series_data(data)
99
+ if data.respond_to?(:group_by_day)
100
+ daily_data = data.group_by_day(:created_at).count
101
+ {
102
+ series: [{
103
+ name: @chart_config[:title] || "Data",
104
+ data: daily_data.values
105
+ }],
106
+ categories: daily_data.keys.map { |date| date.strftime("%b %d") }
107
+ }
108
+ else
109
+ { series: [{ name: "Data", data: [data.count] }] }
110
+ end
111
+ end
112
+
113
+ def process_category_data(data)
114
+ if data.respond_to?(:group)
115
+ grouped = data.group(:category).count # Example grouping
116
+ {
117
+ series: [{
118
+ name: @chart_config[:title] || "Count",
119
+ data: grouped.values
120
+ }],
121
+ categories: grouped.keys.map(&:humanize)
122
+ }
123
+ else
124
+ { series: [{ name: "Total", data: [data.count] }] }
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,76 @@
1
+ module EasyAdmin
2
+ class LazyMetricCardComponent < Phlex::HTML
3
+ def initialize(metric_config:, record:)
4
+ @metric_config = metric_config
5
+ @record = record
6
+ end
7
+
8
+ def view_template
9
+ div(class: @metric_config[:css_classes]) do
10
+ div(class: "card-body") do
11
+ div(class: "counter-status d-flex md-mb-0") do
12
+ div(class: "counter-icon bg-#{@metric_config[:color]}-transparent") do
13
+ if @metric_config[:icon]
14
+ unsafe_raw "<i class=\"#{@metric_config[:icon]} text-#{@metric_config[:color]}\"></i>"
15
+ end
16
+ end
17
+
18
+ div(class: "ms-auto") do
19
+ h5(class: "fs-13") { @metric_config[:title] }
20
+ h2(class: "mb-0 fs-22 mb-1 mt-1") do
21
+ render_metric_value
22
+ end
23
+
24
+ if @metric_config[:trend]
25
+ p(class: "text-muted mb-0 fs-11") do
26
+ trend_icon = trend_positive?(@metric_config[:trend]) ? "si si-arrow-up-circle text-success" : "si si-arrow-down-circle text-danger"
27
+ unsafe_raw "<i class=\"#{trend_icon} me-1\"></i>#{@metric_config[:trend]}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def render_metric_value
39
+ case @metric_config[:value]
40
+ when Symbol
41
+ # Dynamic value from record
42
+ value = @record.public_send(@metric_config[:value])
43
+ format_metric_value(value)
44
+ when Proc
45
+ # Execute proc with record context
46
+ value = @metric_config[:value].call(@record)
47
+ format_metric_value(value)
48
+ else
49
+ # Static value
50
+ format_metric_value(@metric_config[:value])
51
+ end
52
+ end
53
+
54
+ def format_metric_value(value)
55
+ case @metric_config[:format]
56
+ when :currency
57
+ "$#{number_with_delimiter(value)}"
58
+ when :percentage
59
+ "#{value}%"
60
+ when :number
61
+ number_with_delimiter(value)
62
+ else
63
+ value.to_s
64
+ end
65
+ end
66
+
67
+ def trend_positive?(trend_text)
68
+ positive_words = %w[increase up growth positive gain]
69
+ positive_words.any? { |word| trend_text.downcase.include?(word) }
70
+ end
71
+
72
+ def number_with_delimiter(value)
73
+ value.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,26 @@
1
+ module EasyAdmin
2
+ class ModalFrameComponent < BaseComponent
3
+ def view_template
4
+ # Modal backdrop
5
+ div(
6
+ id: "modal-backdrop",
7
+ class: "fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 opacity-0 transition-opacity duration-300",
8
+ style: "display: none;",
9
+ data: {
10
+ controller: "modal",
11
+ action: "click->modal#closeOnBackdrop"
12
+ }
13
+ ) do
14
+ div(class: "relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 xl:w-2/5 shadow-lg rounded-md bg-white") do
15
+ # Modal content will be loaded here via turbo-frame
16
+ turbo_frame(id: "modal") do
17
+ div(class: "text-center p-8") do
18
+ div(class: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto") { }
19
+ p(class: "mt-4 text-gray-500") { "Loading..." }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,226 @@
1
+ module EasyAdmin
2
+ class NavbarComponent < Phlex::HTML
3
+ def initialize(page_title: "Dashboard", breadcrumbs: nil, current_user: nil)
4
+ @page_title = page_title
5
+ @breadcrumbs = breadcrumbs
6
+ @current_user = current_user
7
+ end
8
+
9
+ def view_template
10
+ header(
11
+ class: "bg-white border-b border-gray-200 px-6 py-4 lg:ml-64 transition-all duration-300 fixed top-0 right-0 left-0 z-30",
12
+ data: {
13
+ controller: "navbar-scroll",
14
+ navbar_scroll_target: "navbar"
15
+ }
16
+ ) do
17
+ div(class: "flex items-center justify-between") do
18
+ # Left side - Sidebar toggle + Page title and breadcrumbs
19
+ render_left_section
20
+
21
+ # Right side - Actions and user menu
22
+ render_right_section
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def render_left_section
30
+ div(class: "flex items-center space-x-4") do
31
+ # Sidebar toggle button (desktop)
32
+ button(
33
+ class: "hidden lg:block p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
34
+ title: "Toggle sidebar",
35
+ data: { action: "click->sidebar#toggle" }
36
+ ) do
37
+ unsafe_raw('<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>')
38
+ end
39
+
40
+ div(class: "flex items-center space-x-3") do
41
+ h1(
42
+ class: "hidden md:block text-xl font-semibold text-gray-900 transition-opacity duration-300",
43
+ data: { navbar_scroll_target: "content" }
44
+ ) { @page_title }
45
+
46
+ if @breadcrumbs&.any?
47
+ nav(
48
+ class: "hidden md:flex transition-opacity duration-300",
49
+ aria_label: "Breadcrumb",
50
+ data: { navbar_scroll_target: "content" }
51
+ ) do
52
+ ol(class: "flex items-center space-x-2 text-sm text-gray-500") do
53
+ @breadcrumbs.each_with_index do |crumb, index|
54
+ li do
55
+ if index > 0
56
+ span(class: "mx-2 text-gray-400") { "/" }
57
+ end
58
+
59
+ if crumb[:url]
60
+ a(href: crumb[:url], class: "hover:text-gray-700") { crumb[:label] }
61
+ else
62
+ span(class: "text-gray-900") { crumb[:label] }
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def render_right_section
74
+ div(class: "flex items-center space-x-4") do
75
+ # Notifications button
76
+ div(
77
+ class: "transition-opacity duration-300",
78
+ data: { navbar_scroll_target: "content" }
79
+ ) do
80
+ render_icon_button("notifications", notification_icon)
81
+ end
82
+
83
+ # Search button
84
+ div(
85
+ class: "transition-opacity duration-300",
86
+ data: { navbar_scroll_target: "content" }
87
+ ) do
88
+ render_icon_button("search", search_icon)
89
+ end
90
+
91
+ # Settings button - only if global_settings enabled and Flipper available
92
+ if settings_enabled?
93
+ div(
94
+ class: "transition-opacity duration-300",
95
+ data: { navbar_scroll_target: "content" }
96
+ ) do
97
+ render_settings_button
98
+ end
99
+ end
100
+
101
+ # User menu dropdown - only hide the text part
102
+ render_user_menu
103
+ end
104
+ end
105
+
106
+ def render_icon_button(name, icon_svg)
107
+ button(
108
+ class: "p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
109
+ title: name.capitalize
110
+ ) do
111
+ unsafe_raw(icon_svg)
112
+ end
113
+ end
114
+
115
+ def render_settings_button
116
+ button(
117
+ class: "p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
118
+ title: "Settings",
119
+ data: {
120
+ controller: "event-emitter",
121
+ event_emitter_event_value: "settings:open",
122
+ action: "click->event-emitter#emit"
123
+ }
124
+ ) do
125
+ unsafe_raw(settings_icon)
126
+ end
127
+ end
128
+
129
+ def render_user_menu
130
+ div(class: "relative", data: { controller: "dropdown" }) do
131
+ # User menu trigger
132
+ button(
133
+ class: "flex items-center space-x-3 p-2 text-sm bg-white rounded-lg hover:bg-gray-50 focus:outline-none transition-all duration-300",
134
+ data: {
135
+ action: "click->dropdown#toggle",
136
+ dropdown_target: "trigger"
137
+ }
138
+ ) do
139
+ # User avatar
140
+ div(class: "w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center") do
141
+ span(class: "text-white font-medium text-sm") { user_initials }
142
+ end
143
+
144
+ # User info (hidden on mobile and when scrolled)
145
+ div(
146
+ class: "hidden md:block text-left transition-opacity duration-300",
147
+ data: { navbar_scroll_target: "content" }
148
+ ) do
149
+ div(class: "font-medium text-gray-900") { user_name }
150
+ div(class: "text-xs text-gray-500") { user_role }
151
+ end
152
+
153
+ # Dropdown chevron
154
+ unsafe_raw(chevron_down_icon)
155
+ end
156
+
157
+ # Dropdown menu
158
+ div(
159
+ class: "absolute right-0 z-10 mt-2 w-48 bg-white rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none hidden",
160
+ data: { dropdown_target: "menu" }
161
+ ) do
162
+ div(class: "py-1") do
163
+ render_menu_item("Sign out", logout_icon, "#", "text-red-600 hover:bg-red-50")
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ def render_menu_item(label, icon_svg, url, extra_classes = "text-gray-700 hover:bg-gray-100")
170
+ a(
171
+ href: url,
172
+ class: "block px-4 py-2 text-sm #{extra_classes}"
173
+ ) do
174
+ div(class: "flex items-center space-x-2") do
175
+ unsafe_raw(icon_svg)
176
+ span { label }
177
+ end
178
+ end
179
+ end
180
+
181
+ def user_initials
182
+ if @current_user&.respond_to?(:name)
183
+ @current_user.name.split.map(&:first).join.upcase[0..1]
184
+ else
185
+ "A"
186
+ end
187
+ end
188
+
189
+ def user_name
190
+ @current_user&.respond_to?(:name) ? @current_user.name : "Admin User"
191
+ end
192
+
193
+ def user_role
194
+ @current_user&.respond_to?(:role) ? @current_user.role.capitalize : "Administrator"
195
+ end
196
+
197
+ # SVG Icons
198
+ def notification_icon
199
+ '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-5-5v5zM10.06 9.06A4 4 0 0 1 14 5.5a4 4 0 0 1 4 4 4 4 0 0 1-.44 1.81l3.94 3.94L20 16l-3.5-3.5A8 8 0 0 1 4 10a8 8 0 0 1 6.06-.94z"/></svg>'
200
+ end
201
+
202
+ def search_icon
203
+ '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>'
204
+ end
205
+
206
+ def settings_icon
207
+ '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>'
208
+ end
209
+
210
+ def chevron_down_icon
211
+ '<svg class="w-4 h-4 text-gray-400 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-dropdown-target="chevron"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>'
212
+ end
213
+
214
+ def profile_icon
215
+ '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>'
216
+ end
217
+
218
+ def logout_icon
219
+ '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>'
220
+ end
221
+
222
+ def settings_enabled?
223
+ EasyAdmin.configuration.global_settings && defined?(Flipper)
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,83 @@
1
+ module EasyAdmin
2
+ class NotificationComponent < Phlex::HTML
3
+ def initialize(type:, message:, title: nil, duration: 4000)
4
+ @type = type
5
+ @message = message
6
+ @title = title
7
+ @duration = duration
8
+ end
9
+
10
+ def view_template
11
+ unsafe_raw("<turbo-frame id=\"notifications\">")
12
+ div(
13
+ class: "fixed top-4 right-4 max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 transform translate-x-full opacity-0 transition-all duration-300 ease-in-out z-50",
14
+ data: {
15
+ controller: "notification",
16
+ notification_type_value: @type,
17
+ notification_message_value: @message,
18
+ notification_duration_value: @duration
19
+ }
20
+ ) do
21
+ div(class: "p-4") do
22
+ div(class: "flex items-start") do
23
+ div(class: "flex-shrink-0") do
24
+ render_icon
25
+ end
26
+
27
+ div(class: "ml-3 w-0 flex-1") do
28
+ if @title
29
+ p(class: "text-sm font-medium text-gray-900") { @title }
30
+ end
31
+
32
+ p(class: notification_text_classes) { @message }
33
+ end
34
+
35
+ div(class: "ml-4 flex-shrink-0 flex") do
36
+ button(
37
+ type: "button",
38
+ class: "bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
39
+ data: { action: "click->notification#close" }
40
+ ) do
41
+ span(class: "sr-only") { "Close" }
42
+ unsafe_raw('<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>')
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ unsafe_raw("</turbo-frame>")
49
+ end
50
+
51
+ private
52
+
53
+ def render_icon
54
+ case @type.to_sym
55
+ when :success
56
+ div(class: "w-6 h-6 text-green-400") do
57
+ unsafe_raw('<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>')
58
+ end
59
+ when :error
60
+ div(class: "w-6 h-6 text-red-400") do
61
+ unsafe_raw('<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>')
62
+ end
63
+ when :warning
64
+ div(class: "w-6 h-6 text-yellow-400") do
65
+ unsafe_raw('<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>')
66
+ end
67
+ else # info
68
+ div(class: "w-6 h-6 text-blue-400") do
69
+ unsafe_raw('<svg fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>')
70
+ end
71
+ end
72
+ end
73
+
74
+ def notification_text_classes
75
+ base_classes = "text-sm"
76
+ if @title
77
+ "#{base_classes} text-gray-500"
78
+ else
79
+ "#{base_classes} text-gray-900"
80
+ end
81
+ end
82
+ end
83
+ end