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,90 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
class CardFactory
|
4
|
+
# Registry for custom card components
|
5
|
+
@custom_components = {}
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :custom_components
|
9
|
+
|
10
|
+
def create_component(card:, card_data:, dashboard_class:)
|
11
|
+
component_class = resolve_component_class(card[:type], card)
|
12
|
+
component_class.new(
|
13
|
+
card: card,
|
14
|
+
card_data: card_data,
|
15
|
+
dashboard_class: dashboard_class
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Register a custom card component
|
20
|
+
# CardFactory.register(:custom_analytics, MyApp::CustomAnalyticsCardComponent)
|
21
|
+
def register(card_type, component_class)
|
22
|
+
@custom_components[card_type.to_sym] = component_class
|
23
|
+
end
|
24
|
+
|
25
|
+
# Unregister a custom card component
|
26
|
+
def unregister(card_type)
|
27
|
+
@custom_components.delete(card_type.to_sym)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def resolve_component_class(card_type, card)
|
33
|
+
# First check if it's a registered custom component
|
34
|
+
if @custom_components.key?(card_type.to_sym)
|
35
|
+
return @custom_components[card_type.to_sym]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Check if card specifies a custom component class
|
39
|
+
if card[:component_class]
|
40
|
+
begin
|
41
|
+
return card[:component_class].constantize
|
42
|
+
rescue NameError => e
|
43
|
+
Rails.logger.warn "Custom card component not found: #{card[:component_class]}, falling back to type-based resolution"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Try to resolve based on card type and naming convention
|
48
|
+
component_class = resolve_by_naming_convention(card_type)
|
49
|
+
return component_class if component_class
|
50
|
+
|
51
|
+
# Check for application-specific component
|
52
|
+
app_component_class = resolve_application_component(card_type)
|
53
|
+
return app_component_class if app_component_class
|
54
|
+
|
55
|
+
# Fallback to CustomCardComponent
|
56
|
+
Rails.logger.warn "Card component not found for type: #{card_type}, falling back to CustomCardComponent"
|
57
|
+
EasyAdmin::Dashboards::Cards::CustomCardComponent
|
58
|
+
end
|
59
|
+
|
60
|
+
def resolve_by_naming_convention(card_type)
|
61
|
+
# Convert card type to component class name
|
62
|
+
# :metric -> MetricCardComponent
|
63
|
+
# :chart -> ChartCardComponent
|
64
|
+
# :table -> TableCardComponent
|
65
|
+
component_name = "#{card_type.to_s.classify}CardComponent"
|
66
|
+
component_class_name = "EasyAdmin::Dashboards::Cards::#{component_name}"
|
67
|
+
|
68
|
+
begin
|
69
|
+
component_class_name.constantize
|
70
|
+
rescue NameError
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def resolve_application_component(card_type)
|
76
|
+
# Check if the application has defined its own card component
|
77
|
+
# This allows apps to override default components
|
78
|
+
# E.g., App::Dashboards::Cards::MetricCardComponent
|
79
|
+
app_component_name = "#{Rails.application.class.module_parent}::Dashboards::Cards::#{card_type.to_s.classify}CardComponent"
|
80
|
+
|
81
|
+
begin
|
82
|
+
app_component_name.constantize
|
83
|
+
rescue NameError
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
class CardStreamComponent < BaseComponent
|
4
|
+
def initialize(card:, card_data:, dashboard_class:)
|
5
|
+
@card = card
|
6
|
+
@card_data = card_data
|
7
|
+
@dashboard_class = dashboard_class
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
# Use factory to dynamically create the appropriate card component
|
12
|
+
card_component = CardFactory.create_component(
|
13
|
+
card: @card,
|
14
|
+
card_data: @card_data,
|
15
|
+
dashboard_class: @dashboard_class
|
16
|
+
)
|
17
|
+
|
18
|
+
render card_component
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
module Cards
|
4
|
+
class BaseCardComponent < BaseComponent
|
5
|
+
def initialize(card:, card_data:, dashboard_class:)
|
6
|
+
@card = card
|
7
|
+
@card_data = card_data
|
8
|
+
@dashboard_class = dashboard_class
|
9
|
+
end
|
10
|
+
|
11
|
+
def view_template
|
12
|
+
turbo_frame(id: "card_#{@card[:name]}") do
|
13
|
+
render_card_content
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :card, :card_data, :dashboard_class
|
20
|
+
|
21
|
+
def render_card_content
|
22
|
+
raise NotImplementedError, "Subclasses must implement #render_card_content"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Common helper methods for all cards
|
26
|
+
def card_title
|
27
|
+
@card[:title] || @card[:name].to_s.humanize
|
28
|
+
end
|
29
|
+
|
30
|
+
def chart_height
|
31
|
+
case @card[:rows]
|
32
|
+
when 1 then 300
|
33
|
+
when 2 then 400
|
34
|
+
else 300
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate_colors(count, alpha = 1.0)
|
39
|
+
colors = [
|
40
|
+
"rgba(59, 130, 246, #{alpha})", # blue
|
41
|
+
"rgba(34, 197, 94, #{alpha})", # green
|
42
|
+
"rgba(249, 115, 22, #{alpha})", # orange
|
43
|
+
"rgba(168, 85, 247, #{alpha})", # purple
|
44
|
+
"rgba(20, 184, 166, #{alpha})", # teal
|
45
|
+
"rgba(236, 72, 153, #{alpha})", # pink
|
46
|
+
"rgba(99, 102, 241, #{alpha})", # indigo
|
47
|
+
"rgba(234, 179, 8, #{alpha})" # yellow
|
48
|
+
]
|
49
|
+
colors.cycle.take(count)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
module Cards
|
4
|
+
class ChartCardComponent < BaseCardComponent
|
5
|
+
private
|
6
|
+
|
7
|
+
def render_card_content
|
8
|
+
div(class: "flex flex-col h-full") do
|
9
|
+
# Chart Content
|
10
|
+
div(class: "flex-1 p-6") do
|
11
|
+
if card_data[:chart_data] && card_data[:chart_data].any?
|
12
|
+
render_chart
|
13
|
+
else
|
14
|
+
render_no_data_state
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Summary Section
|
19
|
+
if card_data[:summary]
|
20
|
+
div(class: "px-6 py-4 bg-gray-50 border-t border-gray-200") do
|
21
|
+
p(class: "text-sm text-gray-700") { card_data[:summary] }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def render_chart
|
28
|
+
chart_type = (@card[:chart_type] || :line).to_s
|
29
|
+
chart_data = prepare_chart_data_for_type(chart_type)
|
30
|
+
|
31
|
+
div(
|
32
|
+
class: "ea-chart-container",
|
33
|
+
data: {
|
34
|
+
controller: "chart",
|
35
|
+
chart_type_value: chart_type,
|
36
|
+
chart_data_value: chart_data.to_json
|
37
|
+
},
|
38
|
+
style: "position: relative; height: #{chart_height}px;"
|
39
|
+
) do
|
40
|
+
unsafe_raw "<canvas></canvas>"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def render_no_data_state
|
45
|
+
div(class: "flex flex-col items-center justify-center py-8 text-gray-500") do
|
46
|
+
unsafe_raw <<~SVG
|
47
|
+
<svg class="w-12 h-12 mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
48
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
49
|
+
</svg>
|
50
|
+
SVG
|
51
|
+
p(class: "text-sm") { "No chart data available" }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def prepare_chart_data_for_type(chart_type)
|
56
|
+
case chart_type
|
57
|
+
when 'line', 'area', 'bar', 'horizontal_bar'
|
58
|
+
prepare_basic_chart_data
|
59
|
+
when 'pie', 'donut'
|
60
|
+
prepare_pie_chart_data
|
61
|
+
when 'scatter'
|
62
|
+
prepare_scatter_chart_data
|
63
|
+
when 'bubble'
|
64
|
+
prepare_bubble_chart_data
|
65
|
+
when 'radar'
|
66
|
+
prepare_radar_chart_data
|
67
|
+
when 'polar_area'
|
68
|
+
prepare_polar_area_chart_data
|
69
|
+
when 'mixed'
|
70
|
+
prepare_mixed_chart_data
|
71
|
+
else
|
72
|
+
prepare_basic_chart_data
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def prepare_basic_chart_data
|
77
|
+
{
|
78
|
+
labels: card_data[:chart_data].map { |d| d[:date] || d[:label] || "Item #{card_data[:chart_data].index(d) + 1}" },
|
79
|
+
datasets: [{
|
80
|
+
label: 'Dataset 1',
|
81
|
+
data: card_data[:chart_data].map { |d| d[:value] },
|
82
|
+
backgroundColor: 'rgba(59, 130, 246, 0.2)',
|
83
|
+
borderColor: 'rgb(59, 130, 246)',
|
84
|
+
borderWidth: 2
|
85
|
+
}]
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def prepare_pie_chart_data
|
90
|
+
{
|
91
|
+
labels: card_data[:chart_data].map { |d| d[:label] || "Item #{card_data[:chart_data].index(d) + 1}" },
|
92
|
+
datasets: [{
|
93
|
+
data: card_data[:chart_data].map { |d| d[:value] },
|
94
|
+
backgroundColor: generate_colors(card_data[:chart_data].length, 0.8),
|
95
|
+
borderColor: generate_colors(card_data[:chart_data].length),
|
96
|
+
borderWidth: 1
|
97
|
+
}]
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def prepare_scatter_chart_data
|
102
|
+
{
|
103
|
+
datasets: [{
|
104
|
+
label: 'Scatter Dataset',
|
105
|
+
data: card_data[:chart_data].map { |d| { x: d[:x] || d[:date], y: d[:y] || d[:value] } },
|
106
|
+
backgroundColor: 'rgb(59, 130, 246)'
|
107
|
+
}]
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def prepare_bubble_chart_data
|
112
|
+
{
|
113
|
+
datasets: [{
|
114
|
+
label: 'Bubble Dataset',
|
115
|
+
data: card_data[:chart_data].map { |d| { x: d[:x], y: d[:y], r: d[:r] } },
|
116
|
+
backgroundColor: generate_colors(card_data[:chart_data].length, 0.8)
|
117
|
+
}]
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
def prepare_radar_chart_data
|
122
|
+
{
|
123
|
+
labels: card_data[:chart_data].map { |d| d[:date] || d[:label] || "Item #{card_data[:chart_data].index(d) + 1}" },
|
124
|
+
datasets: [{
|
125
|
+
label: 'Dataset 1',
|
126
|
+
data: card_data[:chart_data].map { |d| d[:value] },
|
127
|
+
backgroundColor: 'rgba(59, 130, 246, 0.2)',
|
128
|
+
borderColor: 'rgb(59, 130, 246)',
|
129
|
+
borderWidth: 2,
|
130
|
+
pointBackgroundColor: 'rgb(59, 130, 246)',
|
131
|
+
pointBorderColor: '#fff',
|
132
|
+
pointHoverBackgroundColor: '#fff',
|
133
|
+
pointHoverBorderColor: 'rgb(59, 130, 246)'
|
134
|
+
}]
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
def prepare_polar_area_chart_data
|
139
|
+
{
|
140
|
+
labels: card_data[:chart_data].map { |d| d[:label] || "Item #{card_data[:chart_data].index(d) + 1}" },
|
141
|
+
datasets: [{
|
142
|
+
data: card_data[:chart_data].map { |d| d[:value] },
|
143
|
+
backgroundColor: generate_colors(card_data[:chart_data].length, 0.8),
|
144
|
+
borderColor: generate_colors(card_data[:chart_data].length),
|
145
|
+
borderWidth: 1
|
146
|
+
}]
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
def prepare_mixed_chart_data
|
151
|
+
{
|
152
|
+
labels: card_data[:chart_data].map { |d| d[:date] || d[:label] || "Item #{card_data[:chart_data].index(d) + 1}" },
|
153
|
+
datasets: [
|
154
|
+
{
|
155
|
+
type: 'line',
|
156
|
+
label: 'Line Dataset',
|
157
|
+
data: card_data[:chart_data].map { |d| d[:value] },
|
158
|
+
borderColor: 'rgb(59, 130, 246)',
|
159
|
+
backgroundColor: 'rgba(59, 130, 246, 0.2)'
|
160
|
+
},
|
161
|
+
{
|
162
|
+
type: 'bar',
|
163
|
+
label: 'Bar Dataset',
|
164
|
+
data: card_data[:chart_data].map { |d| (d[:value2] || d[:value] || 0) },
|
165
|
+
backgroundColor: 'rgba(34, 197, 94, 0.8)',
|
166
|
+
borderColor: 'rgb(34, 197, 94)',
|
167
|
+
borderWidth: 1
|
168
|
+
}
|
169
|
+
]
|
170
|
+
}
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
module Cards
|
4
|
+
class CustomCardComponent < BaseCardComponent
|
5
|
+
private
|
6
|
+
|
7
|
+
def render_card_content
|
8
|
+
div(class: "flex flex-col h-full") do
|
9
|
+
# Custom Content
|
10
|
+
div(class: "flex-1 p-6") do
|
11
|
+
if @card[:partial]
|
12
|
+
render_custom_partial
|
13
|
+
else
|
14
|
+
render_default_custom_content
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def render_custom_partial
|
21
|
+
# This would render a custom partial specified in the card configuration
|
22
|
+
# For now, we'll render the default content as fallback
|
23
|
+
render_default_custom_content
|
24
|
+
end
|
25
|
+
|
26
|
+
def render_default_custom_content
|
27
|
+
div(class: "flex flex-col items-center justify-center py-8 text-gray-500") do
|
28
|
+
unsafe_raw <<~SVG
|
29
|
+
<svg class="w-12 h-12 mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
30
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 00-2 2v2a2 2 0 002 2m0 0h14m-14 0a2 2 0 01-2-2V9a2 2 0 012-2"/>
|
31
|
+
</svg>
|
32
|
+
SVG
|
33
|
+
h4(class: "text-lg font-medium text-gray-900 mb-2") { "Custom Card" }
|
34
|
+
p(class: "text-sm text-center") { "This is a custom card component. Override the partial or implement custom logic." }
|
35
|
+
|
36
|
+
# Display any custom data if available
|
37
|
+
if card_data && !card_data.empty?
|
38
|
+
div(class: "mt-4 p-4 bg-gray-100 rounded-lg") do
|
39
|
+
h5(class: "text-sm font-medium text-gray-700 mb-2") { "Card Data:" }
|
40
|
+
pre(class: "text-xs text-gray-600 overflow-auto") do
|
41
|
+
card_data.to_json
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
module Cards
|
4
|
+
class MetricCardComponent < BaseCardComponent
|
5
|
+
private
|
6
|
+
|
7
|
+
def render_card_content
|
8
|
+
div(class: "p-6 flex flex-col h-full") do
|
9
|
+
# Main Metric Value with Trend Color
|
10
|
+
div(class: "flex items-start justify-between mb-4") do
|
11
|
+
div(class: "flex-1") do
|
12
|
+
div(class: metric_value_classes) do
|
13
|
+
number_with_delimiter(card_data[:value] || 0)
|
14
|
+
end
|
15
|
+
div(class: "text-sm text-gray-600 font-medium") do
|
16
|
+
@card[:description] || @card[:title]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Trend Indicator
|
21
|
+
if card_data[:delta]
|
22
|
+
div(class: trend_indicator_classes) do
|
23
|
+
trend_icon
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Sparkline
|
29
|
+
if @card[:show_sparkline] && card_data[:sparkline_data]
|
30
|
+
div(class: "flex-1 mb-4") do
|
31
|
+
render_sparkline
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Delta Information
|
36
|
+
if card_data[:delta]
|
37
|
+
div(class: "mt-auto pt-4 border-t border-gray-100") do
|
38
|
+
div(class: "flex items-center justify-between") do
|
39
|
+
span(class: delta_badge_classes) do
|
40
|
+
trend_icon
|
41
|
+
span(class: "ml-1 font-semibold") { "#{card_data[:delta].abs}%" }
|
42
|
+
end
|
43
|
+
span(class: "text-xs text-gray-500") do
|
44
|
+
"vs #{card_data[:compare_period] || 'last period'}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Helper methods matching the ERB template
|
53
|
+
def metric_value_classes
|
54
|
+
base_classes = "text-3xl font-bold mb-1"
|
55
|
+
if card_data[:delta]
|
56
|
+
case trend_direction
|
57
|
+
when :positive
|
58
|
+
"#{base_classes} text-green-600"
|
59
|
+
when :negative
|
60
|
+
"#{base_classes} text-red-600"
|
61
|
+
else
|
62
|
+
"#{base_classes} text-gray-900"
|
63
|
+
end
|
64
|
+
else
|
65
|
+
"#{base_classes} text-gray-900"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def trend_indicator_classes
|
70
|
+
base_classes = "flex items-center justify-center w-10 h-10 rounded-full"
|
71
|
+
case trend_direction
|
72
|
+
when :positive
|
73
|
+
"#{base_classes} bg-green-100 text-green-600"
|
74
|
+
when :negative
|
75
|
+
"#{base_classes} bg-red-100 text-red-600"
|
76
|
+
else
|
77
|
+
"#{base_classes} bg-gray-100 text-gray-600"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def delta_badge_classes
|
82
|
+
base_classes = "inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
|
83
|
+
case trend_direction
|
84
|
+
when :positive
|
85
|
+
"#{base_classes} bg-green-100 text-green-800"
|
86
|
+
when :negative
|
87
|
+
"#{base_classes} bg-red-100 text-red-800"
|
88
|
+
else
|
89
|
+
"#{base_classes} bg-gray-100 text-gray-600"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def trend_direction
|
94
|
+
return :neutral unless card_data[:delta]
|
95
|
+
card_data[:delta].to_f > 0 ? :positive : (card_data[:delta].to_f < 0 ? :negative : :neutral)
|
96
|
+
end
|
97
|
+
|
98
|
+
def trend_icon
|
99
|
+
case trend_direction
|
100
|
+
when :positive
|
101
|
+
"↗"
|
102
|
+
when :negative
|
103
|
+
"↘"
|
104
|
+
else
|
105
|
+
"→"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def sparkline_color
|
110
|
+
case trend_direction
|
111
|
+
when :positive
|
112
|
+
"#059669" # green-600
|
113
|
+
when :negative
|
114
|
+
"#dc2626" # red-600
|
115
|
+
else
|
116
|
+
"#4f46e5" # indigo-600
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def sparkline_points
|
121
|
+
return "" unless card_data[:sparkline_data]&.any?
|
122
|
+
|
123
|
+
data = card_data[:sparkline_data]
|
124
|
+
max_val = data.map { |d| d[:value] }.max
|
125
|
+
min_val = data.map { |d| d[:value] }.min
|
126
|
+
range = max_val - min_val
|
127
|
+
range = 1 if range == 0
|
128
|
+
|
129
|
+
data.map.with_index do |point, i|
|
130
|
+
x = i * (100.0 / (data.length - 1))
|
131
|
+
y = 25 - ((point[:value] - min_val) / range.to_f * 20)
|
132
|
+
"#{x},#{y}"
|
133
|
+
end.join(' ')
|
134
|
+
end
|
135
|
+
|
136
|
+
def render_sparkline
|
137
|
+
points = sparkline_points
|
138
|
+
color = sparkline_color
|
139
|
+
gradient_id = "sparkline-gradient-#{@card[:name]}"
|
140
|
+
|
141
|
+
unsafe_raw <<~SVG
|
142
|
+
<svg viewBox="0 0 100 30" class="w-full h-12">
|
143
|
+
<defs>
|
144
|
+
<linearGradient id="#{gradient_id}" x1="0%" y1="0%" x2="0%" y2="100%">
|
145
|
+
<stop offset="0%" style="stop-color:#{color};stop-opacity:0.3" />
|
146
|
+
<stop offset="100%" style="stop-color:#{color};stop-opacity:0" />
|
147
|
+
</linearGradient>
|
148
|
+
</defs>
|
149
|
+
<polyline
|
150
|
+
points="#{points}"
|
151
|
+
stroke="#{color}"
|
152
|
+
stroke-width="2"
|
153
|
+
fill="none"
|
154
|
+
class="drop-shadow-sm"/>
|
155
|
+
<polygon
|
156
|
+
points="#{points},100,30 0,30"
|
157
|
+
fill="url(##{gradient_id})"/>
|
158
|
+
</svg>
|
159
|
+
SVG
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|