easy-admin-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/builds/easy_admin.base.js +43505 -0
- data/app/assets/builds/easy_admin.base.js.map +7 -0
- data/app/assets/builds/easy_admin.css +6141 -0
- data/app/assets/config/easy_admin_manifest.js +1 -0
- data/app/assets/images/jsoneditor-icons.svg +749 -0
- data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
- data/app/components/easy_admin/base_component.rb +35 -0
- data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
- data/app/components/easy_admin/batch_action_form_component.rb +124 -0
- data/app/components/easy_admin/combined_filters_component.rb +232 -0
- data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
- data/app/components/easy_admin/context_menu_component.rb +161 -0
- data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
- data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
- data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
- data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
- data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
- data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
- data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
- data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
- data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
- data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
- data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
- data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
- data/app/components/easy_admin/dashboards/show_component.rb +163 -0
- data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
- data/app/components/easy_admin/date_picker_component.rb +188 -0
- data/app/components/easy_admin/fields/base_component.rb +101 -0
- data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
- data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
- data/app/components/easy_admin/fields/form/date_component.rb +55 -0
- data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
- data/app/components/easy_admin/fields/form/email_component.rb +55 -0
- data/app/components/easy_admin/fields/form/file_component.rb +190 -0
- data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
- data/app/components/easy_admin/fields/form/json_component.rb +81 -0
- data/app/components/easy_admin/fields/form/number_component.rb +55 -0
- data/app/components/easy_admin/fields/form/select_component.rb +326 -0
- data/app/components/easy_admin/fields/form/text_component.rb +55 -0
- data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
- data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
- data/app/components/easy_admin/fields/index/date_component.rb +13 -0
- data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/index/email_component.rb +24 -0
- data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
- data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
- data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
- data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
- data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
- data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
- data/app/components/easy_admin/fields/index/json_component.rb +23 -0
- data/app/components/easy_admin/fields/index/number_component.rb +20 -0
- data/app/components/easy_admin/fields/index/select_component.rb +25 -0
- data/app/components/easy_admin/fields/index/text_component.rb +20 -0
- data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
- data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
- data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
- data/app/components/easy_admin/fields/show/date_component.rb +13 -0
- data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/show/email_component.rb +19 -0
- data/app/components/easy_admin/fields/show/file_component.rb +304 -0
- data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
- data/app/components/easy_admin/fields/show/json_component.rb +45 -0
- data/app/components/easy_admin/fields/show/number_component.rb +20 -0
- data/app/components/easy_admin/fields/show/select_component.rb +25 -0
- data/app/components/easy_admin/fields/show/text_component.rb +17 -0
- data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
- data/app/components/easy_admin/filters_component.rb +120 -0
- data/app/components/easy_admin/form_tabs_component.rb +166 -0
- data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
- data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
- data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
- data/app/components/easy_admin/modal_frame_component.rb +26 -0
- data/app/components/easy_admin/navbar_component.rb +226 -0
- data/app/components/easy_admin/notification_component.rb +83 -0
- data/app/components/easy_admin/pagination_component.rb +188 -0
- data/app/components/easy_admin/quick_filters_component.rb +65 -0
- data/app/components/easy_admin/resource_pagination_component.rb +14 -0
- data/app/components/easy_admin/resources/index_component.rb +211 -0
- data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
- data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
- data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
- data/app/components/easy_admin/resources/table_component.rb +206 -0
- data/app/components/easy_admin/resources/table_row_component.rb +160 -0
- data/app/components/easy_admin/row_action_form_component.rb +127 -0
- data/app/components/easy_admin/scopes_component.rb +224 -0
- data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
- data/app/components/easy_admin/show_layout_component.rb +600 -0
- data/app/components/easy_admin/sidebar_component.rb +174 -0
- data/app/components/easy_admin/turbo/response_component.rb +40 -0
- data/app/components/easy_admin/turbo/stream_component.rb +28 -0
- data/app/controllers/easy_admin/application_controller.rb +66 -0
- data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
- data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
- data/app/controllers/easy_admin/passwords_controller.rb +15 -0
- data/app/controllers/easy_admin/registrations_controller.rb +52 -0
- data/app/controllers/easy_admin/resources_controller.rb +907 -0
- data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
- data/app/controllers/easy_admin/sessions_controller.rb +32 -0
- data/app/controllers/easy_admin/settings_controller.rb +94 -0
- data/app/helpers/easy_admin/application_helper.rb +4 -0
- data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
- data/app/helpers/easy_admin/fields_helper.rb +27 -0
- data/app/helpers/easy_admin/pagy_helper.rb +30 -0
- data/app/helpers/easy_admin/resources_helper.rb +39 -0
- data/app/javascript/easy_admin/application.js +12 -0
- data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
- data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
- data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
- data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
- data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
- data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
- data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
- data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
- data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
- data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
- data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
- data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
- data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
- data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
- data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
- data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
- data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
- data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
- data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
- data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
- data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
- data/app/javascript/easy_admin/controllers.js +54 -0
- data/app/javascript/easy_admin.base.js +4 -0
- data/app/models/easy_admin/admin_user.rb +53 -0
- data/app/models/easy_admin/application_record.rb +5 -0
- data/app/views/easy_admin/dashboard/index.html.erb +3 -0
- data/app/views/easy_admin/dashboards/show.html.erb +7 -0
- data/app/views/easy_admin/passwords/edit.html.erb +42 -0
- data/app/views/easy_admin/passwords/new.html.erb +41 -0
- data/app/views/easy_admin/registrations/new.html.erb +65 -0
- data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
- data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
- data/app/views/easy_admin/resources/edit.html.erb +151 -0
- data/app/views/easy_admin/resources/index.html.erb +12 -0
- data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
- data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
- data/app/views/easy_admin/resources/new.html.erb +100 -0
- data/app/views/easy_admin/resources/show.html.erb +31 -0
- data/app/views/easy_admin/sessions/new.html.erb +55 -0
- data/app/views/easy_admin/settings/_form.html.erb +51 -0
- data/app/views/easy_admin/settings/index.html.erb +53 -0
- data/app/views/layouts/easy_admin/application.html.erb +48 -0
- data/app/views/layouts/easy_admin/auth.html.erb +34 -0
- data/config/initializers/easy_admin_card_factory.rb +27 -0
- data/config/initializers/pagy.rb +15 -0
- data/config/initializers/rack_mini_profiler.rb +67 -0
- data/config/routes.rb +70 -0
- data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
- data/lib/easy-admin.rb +32 -0
- data/lib/easy_admin/action.rb +159 -0
- data/lib/easy_admin/batch_action.rb +134 -0
- data/lib/easy_admin/configuration.rb +75 -0
- data/lib/easy_admin/dashboard.rb +110 -0
- data/lib/easy_admin/dashboard_registry.rb +30 -0
- data/lib/easy_admin/delete_action.rb +22 -0
- data/lib/easy_admin/engine.rb +54 -0
- data/lib/easy_admin/field.rb +118 -0
- data/lib/easy_admin/resource.rb +806 -0
- data/lib/easy_admin/resource_registry.rb +22 -0
- data/lib/easy_admin/types/json_type.rb +25 -0
- data/lib/easy_admin/version.rb +3 -0
- data/lib/generators/easy_admin/auth_generator.rb +69 -0
- data/lib/generators/easy_admin/card/card_generator.rb +94 -0
- data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
- data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
- data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
- data/lib/generators/easy_admin/install_generator.rb +25 -0
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
- data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
- data/lib/generators/easy_admin/resource_generator.rb +43 -0
- data/lib/generators/easy_admin/templates/AUTH_README +35 -0
- data/lib/generators/easy_admin/templates/README +27 -0
- data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
- data/lib/generators/easy_admin/templates/devise.rb +267 -0
- data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
- data/lib/generators/easy_admin/templates/resource.rb +29 -0
- data/lib/tasks/easy_admin_tasks.rake +4 -0
- metadata +445 -0
@@ -0,0 +1,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
|