plutonium 0.64.0 → 0.65.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 +4 -4
- data/.claude/skills/plutonium/SKILL.md +4 -1
- data/.claude/skills/plutonium-app/SKILL.md +14 -0
- data/.claude/skills/plutonium-dashboard/SKILL.md +161 -0
- data/.claude/skills/plutonium-resource/SKILL.md +94 -14
- data/.claude/skills/plutonium-ui/SKILL.md +22 -1
- data/CHANGELOG.md +59 -0
- data/CLAUDE.md +282 -210
- data/SECURITY.md +1 -1
- data/app/assets/plutonium-charts.js +20604 -0
- data/app/assets/plutonium-charts.js.map +7 -0
- data/app/assets/plutonium-charts.min.js +41 -0
- data/app/assets/plutonium-charts.min.js.map +7 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +794 -629
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +48 -48
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/plutonium/_flash_alerts.html.erb +2 -2
- data/app/views/plutonium/_resource_header.html.erb +2 -2
- data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
- data/app/views/plutonium/_toast.html.erb +2 -2
- data/app/views/rodauth/_login_form.html.erb +1 -1
- data/app/views/rodauth/_password_visibility.html.erb +1 -1
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +2 -2
- data/app/views/rodauth/create_account.html.erb +4 -4
- data/app/views/rodauth/reset_password_request.html.erb +1 -1
- data/app/views/rodauth/verify_account_resend.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +1 -1
- data/config/brakeman.ignore +4 -4
- data/config/initializers/rabl.rb +0 -40
- data/config/locales/en/api_client.yml +22 -0
- data/config/locales/en/async.yml +34 -0
- data/config/locales/en/dashboard.yml +25 -0
- data/config/locales/en/invites.yml +37 -0
- data/config/locales/en/js.yml +40 -0
- data/config/locales/en/kanban.yml +28 -0
- data/config/locales/en/plutonium.yml +26 -0
- data/config/locales/en/positioning.yml +14 -0
- data/config/locales/en/profile.yml +29 -0
- data/config/locales/en/resource.yml +60 -0
- data/config/locales/en/ui.yml +93 -0
- data/config/locales/en/wizard.yml +46 -0
- data/docs/.vitepress/config.ts +34 -1
- data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
- data/docs/.vitepress/theme/custom.css +34 -377
- data/docs/blog/introducing-plutonium-dashboards.md +175 -0
- data/docs/blog/introducing-plutonium-i18n.md +91 -0
- data/docs/blog/introducing-plutonium.md +4 -5
- data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
- data/docs/guides/dashboards.md +300 -0
- data/docs/guides/index.md +1 -0
- data/docs/guides/kanban.md +2 -2
- data/docs/public/images/blog/dashboards-card-error.png +0 -0
- data/docs/public/images/blog/dashboards-loading.png +0 -0
- data/docs/public/images/blog/dashboards-overview.png +0 -0
- data/docs/public/images/blog/dashboards-sidebar.png +0 -0
- data/docs/public/images/blog/i18n-es-admin.png +0 -0
- data/docs/public/images/guides/dashboard-card-error.png +0 -0
- data/docs/public/images/guides/dashboard-loading.png +0 -0
- data/docs/public/images/guides/dashboard-overview.png +0 -0
- data/docs/public/images/guides/dashboard-sidebar.png +0 -0
- data/docs/reference/app/generators.md +2 -2
- data/docs/reference/app/portals.md +2 -0
- data/docs/reference/configuration.md +84 -27
- data/docs/reference/dashboard/dsl.md +123 -0
- data/docs/reference/dashboard/index.md +35 -0
- data/docs/reference/dashboard/registration.md +76 -0
- data/docs/reference/i18n.md +239 -0
- data/docs/reference/index.md +10 -1
- data/docs/reference/kanban/dsl.md +1 -1
- data/docs/reference/kanban/positioning.md +2 -2
- data/docs/reference/resource/actions.md +1 -1
- data/docs/reference/resource/definition.md +96 -18
- data/docs/reference/resource/export.md +5 -1
- data/docs/reference/resource/index.md +2 -0
- data/docs/reference/{positioning.md → resource/positioning.md} +2 -1
- data/docs/reference/resource/query.md +2 -0
- data/docs/reference/ui/assets.md +8 -0
- data/docs/reference/ui/displays.md +5 -8
- data/docs/reference/ui/index.md +1 -1
- data/docs/reference/wizard/dsl.md +2 -2
- data/esbuild.config.js +5 -2
- data/gemfiles/rails_7.gemfile.lock +2 -2
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/active_model/validations/array_validator.rb +1 -1
- data/lib/active_model/validations/attached_validator.rb +1 -1
- data/lib/active_model/validations/url_validator.rb +2 -2
- data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
- data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +3 -2
- data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
- data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
- data/lib/generators/pu/docker/install/install_generator.rb +58 -0
- data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
- data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
- data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
- data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
- data/lib/generators/pu/invites/invitable_generator.rb +18 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
- data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
- data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
- data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
- data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
- data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
- data/lib/plutonium/action/base.rb +27 -3
- data/lib/plutonium/action/interactive.rb +17 -4
- data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
- data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
- data/lib/plutonium/auth/sequel_adapter.rb +1 -1
- data/lib/plutonium/configuration.rb +5 -0
- data/lib/plutonium/core/controller.rb +25 -1
- data/lib/plutonium/core/controllers/authorizable.rb +1 -1
- data/lib/plutonium/dashboard/base.rb +114 -0
- data/lib/plutonium/dashboard/card.rb +200 -0
- data/lib/plutonium/dashboard/controller.rb +101 -0
- data/lib/plutonium/dashboard/dsl.rb +104 -0
- data/lib/plutonium/dashboard/register.rb +40 -0
- data/lib/plutonium/dashboard/route_resolution.rb +30 -0
- data/lib/plutonium/dashboard.rb +42 -0
- data/lib/plutonium/definition/actions.rb +6 -1
- data/lib/plutonium/definition/base.rb +41 -3
- data/lib/plutonium/definition/index_views.rb +1 -1
- data/lib/plutonium/definition/positioning.rb +1 -1
- data/lib/plutonium/definition/presentable.rb +2 -2
- data/lib/plutonium/helpers/assets_helper.rb +15 -2
- data/lib/plutonium/helpers/display_helper.rb +10 -2
- data/lib/plutonium/interaction/async/context.rb +6 -8
- data/lib/plutonium/interaction/async/executor.rb +6 -7
- data/lib/plutonium/interaction/async/run.rb +2 -2
- data/lib/plutonium/interaction/async/run_definition.rb +1 -1
- data/lib/plutonium/interaction/base.rb +2 -0
- data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
- data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
- data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
- data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
- data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
- data/lib/plutonium/invites/controller.rb +11 -11
- data/lib/plutonium/kanban/column.rb +13 -3
- data/lib/plutonium/kanban/dsl.rb +7 -4
- data/lib/plutonium/models/has_cents.rb +1 -1
- data/lib/plutonium/profile/security_section.rb +10 -18
- data/lib/plutonium/query/filter.rb +10 -1
- data/lib/plutonium/query/filters/association.rb +27 -2
- data/lib/plutonium/query/filters/boolean.rb +4 -4
- data/lib/plutonium/query/filters/date.rb +4 -17
- data/lib/plutonium/query/filters/date_range.rb +2 -2
- data/lib/plutonium/query/filters/select.rb +10 -1
- data/lib/plutonium/query/filters/text.rb +9 -12
- data/lib/plutonium/railtie.rb +17 -0
- data/lib/plutonium/resource/controller.rb +10 -5
- data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
- data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
- data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
- data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
- data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
- data/lib/plutonium/resource/query_object.rb +30 -3
- data/lib/plutonium/routing/dashboard_registration.rb +137 -0
- data/lib/plutonium/routing/resource_registration.rb +5 -0
- data/lib/plutonium/routing/route_set_extensions.rb +2 -1
- data/lib/plutonium/translation.rb +227 -0
- data/lib/plutonium/ui/actions_dropdown.rb +2 -2
- data/lib/plutonium/ui/breadcrumbs.rb +3 -3
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/behaviour.rb +46 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/dashboard/board.rb +80 -0
- data/lib/plutonium/ui/dashboard/card.rb +127 -0
- data/lib/plutonium/ui/dashboard/chart.rb +43 -0
- data/lib/plutonium/ui/dashboard/custom.rb +32 -0
- data/lib/plutonium/ui/dashboard/frame.rb +25 -0
- data/lib/plutonium/ui/dashboard/metric.rb +137 -0
- data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
- data/lib/plutonium/ui/display/components/attachment.rb +1 -1
- data/lib/plutonium/ui/display/components/badge.rb +7 -1
- data/lib/plutonium/ui/display/components/boolean.rb +2 -2
- data/lib/plutonium/ui/display/resource.rb +23 -16
- data/lib/plutonium/ui/export_button.rb +3 -3
- data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
- data/lib/plutonium/ui/form/components/password.rb +2 -2
- data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
- data/lib/plutonium/ui/form/components/uppy.rb +11 -3
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
- data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
- data/lib/plutonium/ui/form/query.rb +8 -8
- data/lib/plutonium/ui/form/resource.rb +54 -26
- data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
- data/lib/plutonium/ui/grid/card.rb +3 -3
- data/lib/plutonium/ui/grid/resource.rb +3 -3
- data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
- data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
- data/lib/plutonium/ui/kanban/column.rb +6 -6
- data/lib/plutonium/ui/layout/base.rb +19 -1
- data/lib/plutonium/ui/layout/header.rb +1 -1
- data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
- data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/layout/topbar.rb +2 -2
- data/lib/plutonium/ui/modal/base.rb +4 -4
- data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
- data/lib/plutonium/ui/nav_user.rb +2 -2
- data/lib/plutonium/ui/page/base.rb +10 -2
- data/lib/plutonium/ui/page/dashboard.rb +54 -0
- data/lib/plutonium/ui/page/edit.rb +3 -3
- data/lib/plutonium/ui/page/interactive_action.rb +2 -2
- data/lib/plutonium/ui/page/new.rb +3 -3
- data/lib/plutonium/ui/page/show.rb +2 -2
- data/lib/plutonium/ui/page/wizard.rb +22 -16
- data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
- data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
- data/lib/plutonium/ui/skeleton_table.rb +1 -1
- data/lib/plutonium/ui/table/base.rb +2 -2
- data/lib/plutonium/ui/table/components/attachment.rb +1 -1
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
- data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
- data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
- data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
- data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
- data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
- data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
- data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
- data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
- data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
- data/lib/plutonium/ui/table/resource.rb +53 -17
- data/lib/plutonium/ui/wizard/review.rb +7 -7
- data/lib/plutonium/ui/wizard/stepper.rb +1 -1
- data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/base.rb +2 -0
- data/lib/plutonium/wizard/driving.rb +8 -2
- data/lib/plutonium/wizard/dsl.rb +4 -2
- data/lib/plutonium/wizard/field_importer.rb +1 -8
- data/lib/plutonium/wizard/review_step.rb +8 -1
- data/lib/plutonium/wizard/step.rb +14 -3
- data/package.json +4 -2
- data/plutonium.gemspec +4 -1
- data/postcss-gem-import.cjs +34 -0
- data/postcss-gem-import.js +3 -28
- data/src/css/components.css +91 -0
- data/src/css/slim_select.css +0 -38
- data/src/css/tokens.css +24 -0
- data/src/js/controllers/attachment_input_controller.js +15 -6
- data/src/js/controllers/chart_controller.js +162 -0
- data/src/js/controllers/clipboard_controller.js +3 -2
- data/src/js/controllers/dirty_form_guard_controller.js +4 -3
- data/src/js/controllers/easymde_controller.js +3 -0
- data/src/js/controllers/filter_panel_controller.js +15 -6
- data/src/js/controllers/flatpickr_controller.js +8 -0
- data/src/js/controllers/frame_refresh_controller.js +59 -0
- data/src/js/controllers/intl_tel_input_controller.js +5 -0
- data/src/js/controllers/register_controllers.js +4 -0
- data/src/js/controllers/slim_select_controller.js +79 -176
- data/src/js/core.js +9 -1
- data/src/js/i18n.js +69 -0
- data/src/js/plutonium-charts.js +15 -0
- data/src/js/turbo/turbo_confirm.js +5 -3
- metadata +64 -16
- data/.yarnrc.yml +0 -8
- data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
- data/lib/generators/pu/field/input/input_generator.rb +0 -32
- data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
- data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
- data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
- data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
- data/public/plutonium-assets/application.js +0 -31419
- data/public/plutonium-assets/plutonium.ico +0 -0
- data/yarn.lock +0 -6858
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Wording specific to the <%= package_name %> portal.
|
|
2
|
+
#
|
|
3
|
+
# This file is loaded automatically (the portal is a Rails engine) ahead of the
|
|
4
|
+
# host app's config/locales: anything here overrides Plutonium's defaults for
|
|
5
|
+
# this portal, and the app's own locale files override this.
|
|
6
|
+
#
|
|
7
|
+
# Portal-specific text for a resource goes under
|
|
8
|
+
# plutonium.portals.<%= package_namespace %>, mirroring the global keys:
|
|
9
|
+
#
|
|
10
|
+
# en:
|
|
11
|
+
# plutonium:
|
|
12
|
+
# portals:
|
|
13
|
+
# <%= package_namespace %>:
|
|
14
|
+
# fields:
|
|
15
|
+
# blogging/post:
|
|
16
|
+
# title:
|
|
17
|
+
# placeholder: "Working title"
|
|
18
|
+
# hint: "Shown in the sidebar"
|
|
19
|
+
# actions:
|
|
20
|
+
# blogging/post:
|
|
21
|
+
# publish: "Publish to site"
|
|
22
|
+
# scopes:
|
|
23
|
+
# blogging/post:
|
|
24
|
+
# drafts: "Drafts"
|
|
25
|
+
#
|
|
26
|
+
# See https://radioactive-labs.github.io/plutonium-core/reference/i18n
|
|
27
|
+
en:
|
|
28
|
+
plutonium:
|
|
29
|
+
portals:
|
|
30
|
+
<%= package_namespace %>: {}
|
|
@@ -49,14 +49,6 @@ module Pu
|
|
|
49
49
|
template "definition.rb", File.join("app/definitions", class_path, "#{file_name}_definition.rb")
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
# def create_presenter
|
|
53
|
-
# template "presenter.rb", File.join("app/presenters", class_path, "#{file_name}_presenter.rb")
|
|
54
|
-
# end
|
|
55
|
-
|
|
56
|
-
# def create_query_object
|
|
57
|
-
# template "query_object.rb", File.join("app/query_objects", class_path, "#{file_name}_query_object.rb")
|
|
58
|
-
# end
|
|
59
|
-
|
|
60
52
|
private
|
|
61
53
|
|
|
62
54
|
def default_policy_attributes
|
|
@@ -64,24 +64,6 @@ module Pu
|
|
|
64
64
|
options[feature]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
# Creates a hash of options to pass down options to an invoked sub generator
|
|
68
|
-
def invoke_options
|
|
69
|
-
# These are custom options we want to track.
|
|
70
|
-
extra_options = %i[argon2 mails kitchen_sink defaults]
|
|
71
|
-
# Append them to all the available options from our configuration
|
|
72
|
-
valid_options = configuration.keys.map(&:to_sym).concat extra_options
|
|
73
|
-
# Index map the list with the selection value
|
|
74
|
-
opts = valid_options.map { |opt| [opt, send(:"#{opt}?")] }.to_h.compact
|
|
75
|
-
# True only options. We don't care if they are false.
|
|
76
|
-
%i[api_only force skip pretend quiet].each do |key|
|
|
77
|
-
next unless options[key]
|
|
78
|
-
|
|
79
|
-
opts[key] = options[key]
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
opts
|
|
83
|
-
end
|
|
84
|
-
|
|
85
67
|
def all_selected
|
|
86
68
|
@all_selected ||= configuration.keys.select { |feature| send(:"#{feature}?") }
|
|
87
69
|
end
|
|
@@ -6,11 +6,34 @@ module Plutonium
|
|
|
6
6
|
module Action
|
|
7
7
|
# Base class for all actions in the Plutonium framework.
|
|
8
8
|
class Base
|
|
9
|
-
attr_reader :name, :
|
|
9
|
+
attr_reader :name, :icon, :route_options, :turbo, :color, :category, :position, :return_to, :condition, :link, :button
|
|
10
|
+
|
|
11
|
+
# `label:`, `description:` and `confirmation:` may be lazy translations
|
|
12
|
+
# (`t(...)` in a definition) or any zero-arity proc: resolve per read so
|
|
13
|
+
# the text follows the request locale rather than the class load.
|
|
14
|
+
def label
|
|
15
|
+
Plutonium::Translation.resolve(@label) ||
|
|
16
|
+
Plutonium::Translation.label_for(:action, @resource_class, @name) ||
|
|
17
|
+
@name.to_s.titleize
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def description = Plutonium::Translation.resolve(@description)
|
|
21
|
+
|
|
22
|
+
# A copy of this action bound to `resource_class`, so its default label
|
|
23
|
+
# resolves through plutonium.actions.<model>.<action>. Returns self when
|
|
24
|
+
# already bound.
|
|
25
|
+
def for_resource(resource_class)
|
|
26
|
+
return self if @resource_class || resource_class.nil?
|
|
27
|
+
|
|
28
|
+
dup.tap { |copy| copy.instance_variable_set(:@resource_class, resource_class) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def confirmation = Plutonium::Translation.resolve(@confirmation)
|
|
10
32
|
|
|
11
33
|
def initialize(name, **options)
|
|
12
34
|
@name = name.to_sym
|
|
13
|
-
@label = options[:label]
|
|
35
|
+
@label = options[:label]
|
|
36
|
+
@resource_class = options[:resource_class]
|
|
14
37
|
@description = options[:description]
|
|
15
38
|
@icon = options[:icon] || Phlex::TablerIcons::ChevronRight
|
|
16
39
|
@color = options[:color]
|
|
@@ -152,7 +175,8 @@ module Plutonium
|
|
|
152
175
|
size: @modal_size,
|
|
153
176
|
condition: @condition,
|
|
154
177
|
link: @link,
|
|
155
|
-
button: @button
|
|
178
|
+
button: @button,
|
|
179
|
+
resource_class: @resource_class
|
|
156
180
|
}
|
|
157
181
|
end
|
|
158
182
|
|
|
@@ -19,20 +19,33 @@ module Plutonium
|
|
|
19
19
|
@interaction = interaction
|
|
20
20
|
@immediate = immediate
|
|
21
21
|
|
|
22
|
-
options[:label] ||= interaction.label
|
|
23
|
-
options[:description] ||= interaction.description
|
|
24
22
|
options[:icon] ||= interaction.icon
|
|
25
23
|
options[:turbo_frame] = Plutonium::REMOTE_MODAL_FRAME unless options.key?(:turbo_frame)
|
|
26
24
|
|
|
27
25
|
super(name, **options)
|
|
28
26
|
end
|
|
29
27
|
|
|
28
|
+
# `label:` on the action, then the interaction's explicit `presents
|
|
29
|
+
# label:`, then plutonium.actions.<model>.<action>, then the
|
|
30
|
+
# interaction's class-name default. Resolved per read so lazy
|
|
31
|
+
# translations follow the request locale.
|
|
32
|
+
def label
|
|
33
|
+
Plutonium::Translation.resolve(@label) ||
|
|
34
|
+
Plutonium::Translation.resolve(interaction.presentation_metadata[:label]) ||
|
|
35
|
+
Plutonium::Translation.label_for(:action, @resource_class, @name) ||
|
|
36
|
+
interaction.label
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def description
|
|
40
|
+
Plutonium::Translation.resolve(@description) || interaction.description
|
|
41
|
+
end
|
|
42
|
+
|
|
30
43
|
# Get the confirmation message for the action
|
|
31
44
|
#
|
|
32
45
|
# @return [String, nil] The confirmation message or nil if not applicable
|
|
33
46
|
def confirmation
|
|
34
|
-
return @confirmation unless @confirmation.nil?
|
|
35
|
-
@immediate ? "
|
|
47
|
+
return Plutonium::Translation.resolve(@confirmation) unless @confirmation.nil?
|
|
48
|
+
@immediate ? Plutonium::Translation.t("plutonium.actions.confirm_default", label: label) : nil
|
|
36
49
|
end
|
|
37
50
|
|
|
38
51
|
protected
|
|
@@ -27,7 +27,7 @@ module Plutonium
|
|
|
27
27
|
include Plutonium::Interaction::Concerns::Scoping
|
|
28
28
|
|
|
29
29
|
included do
|
|
30
|
-
presents label: "
|
|
30
|
+
presents label: t("plutonium.api_client.create.label"), icon: Phlex::TablerIcons::Key
|
|
31
31
|
|
|
32
32
|
attribute :login, :string
|
|
33
33
|
|
|
@@ -55,7 +55,7 @@ module Plutonium
|
|
|
55
55
|
)
|
|
56
56
|
)
|
|
57
57
|
rescue ActiveRecord::RecordNotFound => e
|
|
58
|
-
failed(login: "
|
|
58
|
+
failed(login: I18n.t("plutonium.api_client.create.failed", error: e.message))
|
|
59
59
|
rescue => e
|
|
60
60
|
failed(login: e.message)
|
|
61
61
|
end
|
|
@@ -173,16 +173,17 @@ module Plutonium
|
|
|
173
173
|
end
|
|
174
174
|
|
|
175
175
|
p(class: "text-green-700 dark:text-green-300") do
|
|
176
|
-
strong { "
|
|
177
|
-
|
|
176
|
+
strong { t("plutonium.api_client.create.credentials.important_label") }
|
|
177
|
+
whitespace
|
|
178
|
+
plain t("plutonium.api_client.create.credentials.important_text")
|
|
178
179
|
end
|
|
179
180
|
end
|
|
180
181
|
end
|
|
181
182
|
|
|
182
183
|
def render_credentials_card
|
|
183
184
|
div(class: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 space-y-4") do
|
|
184
|
-
render_credential_field("
|
|
185
|
-
render_credential_field("
|
|
185
|
+
render_credential_field(t("plutonium.api_client.create.credentials.login"), @login)
|
|
186
|
+
render_credential_field(t("plutonium.api_client.create.credentials.password"), @password)
|
|
186
187
|
end
|
|
187
188
|
end
|
|
188
189
|
|
|
@@ -201,13 +202,13 @@ module Plutonium
|
|
|
201
202
|
type: "button",
|
|
202
203
|
data: {action: "clipboard#copy"},
|
|
203
204
|
class: "px-3 py-2 text-sm bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
|
204
|
-
) { "
|
|
205
|
+
) { t("plutonium.api_client.create.credentials.copy") }
|
|
205
206
|
end
|
|
206
207
|
end
|
|
207
208
|
end
|
|
208
209
|
|
|
209
210
|
def render_action_buttons
|
|
210
|
-
credentials_text = "
|
|
211
|
+
credentials_text = t("plutonium.api_client.create.credentials.credentials_text", login: @login, password: @password)
|
|
211
212
|
|
|
212
213
|
div(class: "mt-6 flex gap-4", data: {controller: "clipboard"}) do
|
|
213
214
|
input(type: "hidden", value: credentials_text, data: {clipboard_target: "source"})
|
|
@@ -215,12 +216,12 @@ module Plutonium
|
|
|
215
216
|
type: "button",
|
|
216
217
|
data: {action: "clipboard#copy"},
|
|
217
218
|
class: "px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors"
|
|
218
|
-
) { "
|
|
219
|
+
) { t("plutonium.api_client.create.credentials.copy_all") }
|
|
219
220
|
|
|
220
221
|
a(
|
|
221
222
|
href: done_url,
|
|
222
223
|
class: "px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md transition-colors"
|
|
223
|
-
) { "
|
|
224
|
+
) { t("plutonium.api_client.create.credentials.done") }
|
|
224
225
|
end
|
|
225
226
|
end
|
|
226
227
|
|
|
@@ -242,7 +243,7 @@ module Plutonium
|
|
|
242
243
|
|
|
243
244
|
# Override in subclass to customize
|
|
244
245
|
def success_title
|
|
245
|
-
"
|
|
246
|
+
t("plutonium.api_client.create.credentials.success_title")
|
|
246
247
|
end
|
|
247
248
|
|
|
248
249
|
# Override in subclass to customize the done URL
|
|
@@ -20,8 +20,8 @@ module Plutonium
|
|
|
20
20
|
extend ActiveSupport::Concern
|
|
21
21
|
|
|
22
22
|
included do
|
|
23
|
-
presents label: "
|
|
24
|
-
description: "
|
|
23
|
+
presents label: t("plutonium.api_client.disable.label"),
|
|
24
|
+
description: t("plutonium.api_client.disable.description"),
|
|
25
25
|
icon: Phlex::TablerIcons::Ban,
|
|
26
26
|
color: :danger
|
|
27
27
|
|
|
@@ -52,7 +52,7 @@ module Plutonium
|
|
|
52
52
|
# @param login [String] the login of the disabled API client
|
|
53
53
|
# @return [String]
|
|
54
54
|
def success_message(login)
|
|
55
|
-
"
|
|
55
|
+
I18n.t("plutonium.api_client.disable.disabled", login: login)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def rodauth_instance
|
|
@@ -14,7 +14,7 @@ module Plutonium
|
|
|
14
14
|
"mysql2" => (RUBY_ENGINE == "jruby") ? "mysql" : "mysql2",
|
|
15
15
|
"sqlite3" => "sqlite",
|
|
16
16
|
"oracle_enhanced" => "oracle",
|
|
17
|
-
"sqlserver" => (RUBY_ENGINE == "jruby") ? "
|
|
17
|
+
"sqlserver" => (RUBY_ENGINE == "jruby") ? "sqlserver" : "tinytds"
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
20
|
class << self
|
|
@@ -209,12 +209,17 @@ module Plutonium
|
|
|
209
209
|
# @return [String] path to JavaScript file
|
|
210
210
|
attr_accessor :script
|
|
211
211
|
|
|
212
|
+
# @return [String] path to the on-demand charts bundle (Chart.js +
|
|
213
|
+
# Chartkick), loaded by the first chart card on a page
|
|
214
|
+
attr_accessor :charts_script
|
|
215
|
+
|
|
212
216
|
# Initialize a new AssetConfiguration instance with default values
|
|
213
217
|
def initialize
|
|
214
218
|
@logo = "plutonium.png"
|
|
215
219
|
@favicon = "plutonium.ico"
|
|
216
220
|
@stylesheet = "plutonium.css"
|
|
217
221
|
@script = "plutonium.min.js"
|
|
222
|
+
@charts_script = "plutonium-charts.min.js"
|
|
218
223
|
end
|
|
219
224
|
end
|
|
220
225
|
end
|
|
@@ -32,6 +32,12 @@ module Plutonium
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Convention lookups (field placeholders, action labels...) layer a
|
|
36
|
+
# portal-specific key over the global one. Publish the portal once per
|
|
37
|
+
# request so lookups made outside a view (query object, kanban flash)
|
|
38
|
+
# resolve the same way the view does.
|
|
39
|
+
before_action { Plutonium::Translation::Current.portal = current_package }
|
|
40
|
+
|
|
35
41
|
before_action do
|
|
36
42
|
next unless defined?(ActiveStorage)
|
|
37
43
|
|
|
@@ -45,7 +51,7 @@ module Plutonium
|
|
|
45
51
|
|
|
46
52
|
append_view_path File.expand_path("app/views", Plutonium.root)
|
|
47
53
|
layout -> { turbo_frame_request? ? false : "resource" }
|
|
48
|
-
helper_method :registered_resources
|
|
54
|
+
helper_method :registered_resources, :registered_dashboards, :dashboard_path_for
|
|
49
55
|
|
|
50
56
|
class_attribute :_rail_enabled, instance_writer: false, default: nil
|
|
51
57
|
class_attribute :_shell, instance_writer: false, default: nil
|
|
@@ -402,6 +408,24 @@ module Plutonium
|
|
|
402
408
|
current_engine.resource_register.resources
|
|
403
409
|
end
|
|
404
410
|
|
|
411
|
+
# Dashboards mounted on the current engine with `register_dashboard`,
|
|
412
|
+
# in registration order.
|
|
413
|
+
def registered_dashboards
|
|
414
|
+
current_engine.dashboard_register.dashboards
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# The page path of a registered dashboard, threading the entity scope
|
|
418
|
+
# segment through on a `:path`-scoped portal. Nil when the dashboard is
|
|
419
|
+
# not registered on the current engine.
|
|
420
|
+
def dashboard_path_for(dashboard_class)
|
|
421
|
+
name = Plutonium::Dashboard::RouteResolution.route_name(current_engine.routes, dashboard_class, action: "show")
|
|
422
|
+
return if name.nil?
|
|
423
|
+
|
|
424
|
+
options = {}
|
|
425
|
+
options[scoped_entity_param_key] = params[scoped_entity_param_key] if scoped_to_entity?
|
|
426
|
+
current_engine.routes.url_helpers.public_send(:"#{name}_path", **options)
|
|
427
|
+
end
|
|
428
|
+
|
|
405
429
|
# Converts RouteOptions into a URL using the appropriate URL resolver.
|
|
406
430
|
#
|
|
407
431
|
# This method takes a RouteOptions object and generates a URL based on the url_resolver
|
|
@@ -20,7 +20,7 @@ module Plutonium
|
|
|
20
20
|
# Use this instead of authorized_scope directly
|
|
21
21
|
def authorized_resource_scope(resource, relation: nil, **options)
|
|
22
22
|
unless resource.instance_of?(Class) && resource < ActiveRecord::Base
|
|
23
|
-
raise ArgumentError
|
|
23
|
+
raise ArgumentError, "Expected resource to be a class inheriting ActiveRecord::Base"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
options[:with] ||= ::ActionPolicy.lookup(resource, namespace: authorization_namespace)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Dashboard
|
|
5
|
+
# The base class every dashboard subclasses.
|
|
6
|
+
#
|
|
7
|
+
# class SalesDashboard < Plutonium::Dashboard::Base
|
|
8
|
+
# presents label: "Sales", description: "Orders and revenue at a glance"
|
|
9
|
+
# refresh 60
|
|
10
|
+
#
|
|
11
|
+
# metric(:orders, icon: Phlex::TablerIcons::ShoppingCart) { orders.count }
|
|
12
|
+
# chart(:revenue, type: :area, span: 8) { orders.group_by_day(:created_at).sum(:total) }
|
|
13
|
+
#
|
|
14
|
+
# private
|
|
15
|
+
#
|
|
16
|
+
# def orders = authorized_resource_scope(Order)
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# An instance is built per request with the view context, so card blocks
|
|
20
|
+
# run with `current_user`, `current_scoped_entity`, `params`,
|
|
21
|
+
# `authorized_resource_scope`, `resource_url_for` and the dashboard's own
|
|
22
|
+
# methods in scope. `authorize?` gates the page and every card endpoint.
|
|
23
|
+
class Base
|
|
24
|
+
extend Plutonium::Translation::Lazy
|
|
25
|
+
include Plutonium::Definition::Presentable
|
|
26
|
+
include DSL
|
|
27
|
+
|
|
28
|
+
attr_reader :view_context
|
|
29
|
+
|
|
30
|
+
# Everything a card block is likely to reach for, forwarded to the view
|
|
31
|
+
# context (which in turn exposes the controller's helper methods).
|
|
32
|
+
delegate :current_user, :current_scoped_entity, :scoped_to_entity?,
|
|
33
|
+
:params, :request, :controller, :current_engine,
|
|
34
|
+
:resource_url_for, :authorized_resource_scope, :allowed_to?, :policy_for,
|
|
35
|
+
:registered_resources, :root_path,
|
|
36
|
+
to: :view_context
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# `plutonium.dashboards.<key>` locale segment and frame/DOM prefix:
|
|
40
|
+
# `AdminPortal::SalesDashboard` → `admin_portal/sales`.
|
|
41
|
+
def i18n_key
|
|
42
|
+
name.underscore.delete_suffix("_dashboard")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Route slug used when `register_dashboard` gets no `as:` and mounts at
|
|
46
|
+
# the root: `SalesDashboard` → `sales`.
|
|
47
|
+
def route_name
|
|
48
|
+
name.demodulize.underscore.delete_suffix("_dashboard")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Title: `presents label:`, then the `plutonium.dashboards.<key>.label`
|
|
52
|
+
# convention, then the class name minus "Dashboard".
|
|
53
|
+
def label
|
|
54
|
+
Plutonium::Translation.resolve(presentation_metadata[:label]) ||
|
|
55
|
+
Plutonium::Translation.dashboard_text(self, :label) ||
|
|
56
|
+
name.demodulize.delete_suffix("Dashboard").titleize
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Caption under the title: `presents description:`, then the
|
|
60
|
+
# `plutonium.dashboards.<key>.description` convention.
|
|
61
|
+
def description
|
|
62
|
+
Plutonium::Translation.resolve(presentation_metadata[:description]) ||
|
|
63
|
+
Plutonium::Translation.dashboard_text(self, :description)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Sidebar icon: `presents icon:`, defaulting to a dashboard glyph.
|
|
67
|
+
def icon
|
|
68
|
+
presentation_metadata[:icon] || Phlex::TablerIcons::LayoutDashboard
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def initialize(view_context)
|
|
73
|
+
@view_context = view_context
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Entry authorization. Authors override it; false → 403 on the page and
|
|
77
|
+
# on every card endpoint. Default allow: a portal already sits behind
|
|
78
|
+
# its auth constraint.
|
|
79
|
+
def authorize? = true
|
|
80
|
+
|
|
81
|
+
# The cards whose `condition:` passes for this request, in order.
|
|
82
|
+
def visible_cards
|
|
83
|
+
self.class.cards.select { |card| card.visible?(self) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The card the endpoint was asked for, if declared and visible.
|
|
87
|
+
# @raise [UnknownCardError]
|
|
88
|
+
def visible_card!(key)
|
|
89
|
+
card = self.class.find_card!(key)
|
|
90
|
+
raise UnknownCardError, "#{self.class.name} card #{key.inspect} is not visible" unless card.visible?(self)
|
|
91
|
+
|
|
92
|
+
card
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The refresh interval for a card: its own, else the dashboard default.
|
|
96
|
+
# A card declared `refresh: false` never refreshes.
|
|
97
|
+
def refresh_for(card)
|
|
98
|
+
return nil if card.refresh == false
|
|
99
|
+
|
|
100
|
+
card.refresh || self.class.refresh
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The view context, for helpers a card block wants by name
|
|
104
|
+
# (`helpers.number_to_currency`, route helpers, ...).
|
|
105
|
+
def helpers = view_context
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def t(key, **options)
|
|
110
|
+
Plutonium::Translation.t(key, **options)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Dashboard
|
|
5
|
+
# One declared card: its kind (`:metric`, `:chart` or `:custom`), the
|
|
6
|
+
# presentation options every kind shares, the kind-specific options and
|
|
7
|
+
# the block that produces its data (or, for a custom card, its markup).
|
|
8
|
+
#
|
|
9
|
+
# Built by {DSL} at class-load and frozen; everything request-dependent
|
|
10
|
+
# (label translation, `condition:`, `href:`) is resolved per render.
|
|
11
|
+
class Card
|
|
12
|
+
KINDS = %i[metric chart custom].freeze
|
|
13
|
+
|
|
14
|
+
# The dashboard grid is 12 columns wide, so halves, thirds and quarters
|
|
15
|
+
# all divide evenly. A card spans 1 to 12 of them; `:full` is 12.
|
|
16
|
+
GRID_COLUMNS = 12
|
|
17
|
+
|
|
18
|
+
# The width each kind reads well at when no `span:` is given: four
|
|
19
|
+
# metrics, two charts or two custom cards to a row.
|
|
20
|
+
DEFAULT_SPANS = {metric: 3, chart: 6, custom: 6}.freeze
|
|
21
|
+
|
|
22
|
+
METRIC_FORMATS = %i[number currency percentage human].freeze
|
|
23
|
+
METRIC_POSITIVE = %i[up down].freeze
|
|
24
|
+
CHART_TYPES = %i[line area column bar pie donut scatter].freeze
|
|
25
|
+
|
|
26
|
+
# Options every kind accepts.
|
|
27
|
+
COMMON_OPTIONS = %i[label description icon span lazy refresh condition href].freeze
|
|
28
|
+
METRIC_OPTIONS = %i[format precision unit prefix suffix positive change_label].freeze
|
|
29
|
+
CHART_OPTIONS = %i[type height].freeze
|
|
30
|
+
|
|
31
|
+
attr_reader :key, :kind, :icon, :span, :refresh, :condition, :href, :block, :options, :dashboard_class
|
|
32
|
+
|
|
33
|
+
def initialize(key, kind:, dashboard_class:, block:, **options)
|
|
34
|
+
@key = key.to_sym
|
|
35
|
+
@kind = kind
|
|
36
|
+
@dashboard_class = dashboard_class
|
|
37
|
+
@block = block
|
|
38
|
+
|
|
39
|
+
raise ArgumentError, "card #{@key.inspect}: unknown kind #{kind.inspect}" unless KINDS.include?(kind)
|
|
40
|
+
raise ArgumentError, "card #{@key.inspect} requires a block" if block.nil?
|
|
41
|
+
|
|
42
|
+
common = options.slice(*COMMON_OPTIONS)
|
|
43
|
+
specific = options.except(*COMMON_OPTIONS)
|
|
44
|
+
|
|
45
|
+
@label = common[:label]
|
|
46
|
+
@description = common[:description]
|
|
47
|
+
@icon = common[:icon]
|
|
48
|
+
@span = validate_span!(common.fetch(:span) { DEFAULT_SPANS.fetch(kind) })
|
|
49
|
+
@lazy = common.fetch(:lazy, true) ? true : false
|
|
50
|
+
@refresh = validate_refresh!(common[:refresh])
|
|
51
|
+
@condition = common[:condition]
|
|
52
|
+
@href = common[:href]
|
|
53
|
+
@options = validate_options!(specific).freeze
|
|
54
|
+
|
|
55
|
+
if @refresh && !@lazy
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"card #{@key.inspect}: `refresh:` reloads the card's turbo frame, so it needs `lazy: true` (the default)"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def lazy? = @lazy
|
|
64
|
+
def metric? = kind == :metric
|
|
65
|
+
def chart? = kind == :chart
|
|
66
|
+
def custom? = kind == :custom
|
|
67
|
+
def full_width? = span == GRID_COLUMNS
|
|
68
|
+
|
|
69
|
+
# The card title, resolved per request: an explicit `label:` (lazy `t`
|
|
70
|
+
# procs included), then the `plutonium.dashboards.<dashboard>.cards.<key>.label`
|
|
71
|
+
# convention, then the key titleized.
|
|
72
|
+
def label
|
|
73
|
+
Plutonium::Translation.resolve(@label) ||
|
|
74
|
+
Plutonium::Translation.dashboard_text(dashboard_class, :cards, key, :label) ||
|
|
75
|
+
key.to_s.titleize
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Optional caption; `description:` first, then the locale convention.
|
|
79
|
+
def description
|
|
80
|
+
Plutonium::Translation.resolve(@description) ||
|
|
81
|
+
Plutonium::Translation.dashboard_text(dashboard_class, :cards, key, :description)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Whether the card renders for this request. `condition:` is a proc
|
|
85
|
+
# evaluated on the dashboard instance (zero arity) or with it as the
|
|
86
|
+
# argument (one arity), or a symbol naming a dashboard method. It gates
|
|
87
|
+
# the page AND the card endpoint, so a hidden card cannot be fetched
|
|
88
|
+
# directly.
|
|
89
|
+
def visible?(dashboard)
|
|
90
|
+
return true if condition.nil?
|
|
91
|
+
|
|
92
|
+
result = case condition
|
|
93
|
+
when Symbol then dashboard.send(condition)
|
|
94
|
+
when Proc then evaluate_on(dashboard, condition)
|
|
95
|
+
else condition
|
|
96
|
+
end
|
|
97
|
+
result ? true : false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The card's link target for this request, if any.
|
|
101
|
+
def href_for(dashboard)
|
|
102
|
+
href.is_a?(Proc) ? evaluate_on(dashboard, href) : href
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Runs the data block on the dashboard instance.
|
|
106
|
+
def evaluate(dashboard)
|
|
107
|
+
evaluate_on(dashboard, block)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# DOM id of the turbo frame hosting this card. One dashboard renders per
|
|
111
|
+
# page, so the card key is unique enough.
|
|
112
|
+
def frame_id
|
|
113
|
+
"#{FRAME_PREFIX}-#{key.to_s.dasherize}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Chart cards: the Chartkick constructor name for `type:`.
|
|
117
|
+
def chart_class
|
|
118
|
+
case options[:type]
|
|
119
|
+
when :area then "AreaChart"
|
|
120
|
+
when :column then "ColumnChart"
|
|
121
|
+
when :bar then "BarChart"
|
|
122
|
+
when :pie, :donut then "PieChart"
|
|
123
|
+
when :scatter then "ScatterChart"
|
|
124
|
+
else "LineChart"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Chart cards: Chartkick options passed through to the JS constructor,
|
|
129
|
+
# with the `donut` flag derived from `type: :donut`.
|
|
130
|
+
def chart_options
|
|
131
|
+
passthrough = options.except(*CHART_OPTIONS)
|
|
132
|
+
passthrough = passthrough.merge(donut: true) if options[:type] == :donut
|
|
133
|
+
passthrough
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def evaluate_on(dashboard, callable)
|
|
139
|
+
callable.arity.zero? ? dashboard.instance_exec(&callable) : dashboard.instance_exec(dashboard, &callable)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def validate_span!(span)
|
|
143
|
+
return GRID_COLUMNS if span == :full
|
|
144
|
+
return span if span.is_a?(Integer) && span.between?(1, GRID_COLUMNS)
|
|
145
|
+
|
|
146
|
+
raise ArgumentError, "card #{key.inspect}: span must be an integer from 1 to #{GRID_COLUMNS}, or :full, got #{span.inspect}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# `false` is kept distinct from nil: nil inherits the dashboard's
|
|
150
|
+
# `refresh`, false opts this card out of it.
|
|
151
|
+
def validate_refresh!(refresh)
|
|
152
|
+
return refresh if refresh.nil? || refresh == false
|
|
153
|
+
return refresh.to_i if refresh.respond_to?(:to_i) && refresh.to_i.positive?
|
|
154
|
+
|
|
155
|
+
raise ArgumentError, "card #{key.inspect}: refresh must be a positive number of seconds or false, got #{refresh.inspect}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate_options!(specific)
|
|
159
|
+
case kind
|
|
160
|
+
when :metric then validate_metric_options!(specific)
|
|
161
|
+
when :chart then validate_chart_options!(specific)
|
|
162
|
+
else validate_custom_options!(specific)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def validate_metric_options!(specific)
|
|
167
|
+
unknown = specific.keys - METRIC_OPTIONS
|
|
168
|
+
raise ArgumentError, "metric #{key.inspect}: unknown option(s) #{unknown.inspect}" if unknown.any?
|
|
169
|
+
|
|
170
|
+
format = specific[:format]
|
|
171
|
+
unless format.nil? || format.is_a?(Proc) || METRIC_FORMATS.include?(format)
|
|
172
|
+
raise ArgumentError,
|
|
173
|
+
"metric #{key.inspect}: format must be one of #{METRIC_FORMATS.inspect} or a proc, got #{format.inspect}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
positive = specific.fetch(:positive, :up)
|
|
177
|
+
unless METRIC_POSITIVE.include?(positive)
|
|
178
|
+
raise ArgumentError, "metric #{key.inspect}: positive must be :up or :down, got #{positive.inspect}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
specific.merge(positive:)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def validate_chart_options!(specific)
|
|
185
|
+
type = specific.fetch(:type, :line)
|
|
186
|
+
unless CHART_TYPES.include?(type)
|
|
187
|
+
raise ArgumentError, "chart #{key.inspect}: type must be one of #{CHART_TYPES.inspect}, got #{type.inspect}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
specific.merge(type:, height: specific.fetch(:height, "240px"))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def validate_custom_options!(specific)
|
|
194
|
+
raise ArgumentError, "card #{key.inspect}: unknown option(s) #{specific.keys.inspect}" if specific.any?
|
|
195
|
+
|
|
196
|
+
specific
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|