ruby_ui_admin 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 +165 -0
- data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
- data/app/components/ruby_ui_admin/ui/base.rb +26 -0
- data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
- data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
- data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
- data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
- data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
- data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
- data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
- data/app/components/ruby_ui_admin/ui/select.rb +29 -0
- data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
- data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
- data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
- data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
- data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
- data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
- data/app/components/ruby_ui_admin/ui.rb +12 -0
- data/app/components/ruby_ui_admin/views/action.rb +37 -0
- data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
- data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
- data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
- data/app/components/ruby_ui_admin/views/base.rb +301 -0
- data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
- data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
- data/app/components/ruby_ui_admin/views/docs.rb +91 -0
- data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
- data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
- data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
- data/app/components/ruby_ui_admin/views/form.rb +151 -0
- data/app/components/ruby_ui_admin/views/home.rb +35 -0
- data/app/components/ruby_ui_admin/views/index.rb +324 -0
- data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
- data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
- data/app/components/ruby_ui_admin/views/show.rb +77 -0
- data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
- data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
- data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
- data/app/components/ruby_ui_admin/views/translation.rb +12 -0
- data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
- data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
- data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
- data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
- data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
- data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
- data/config/locales/ruby_ui_admin.en.yml +59 -0
- data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
- data/config/routes/dynamic_routes.rb +11 -0
- data/config/routes.rb +23 -0
- data/docs/README.md +53 -0
- data/docs/actions/overview.md +112 -0
- data/docs/authorization/action-policy.md +72 -0
- data/docs/authorization/adapters.md +101 -0
- data/docs/authorization/explicit-mode.md +45 -0
- data/docs/authorization/field-authorization.md +53 -0
- data/docs/authorization/policies.md +62 -0
- data/docs/authorization/scopes.md +48 -0
- data/docs/customization/controllers.md +62 -0
- data/docs/customization/ejecting.md +55 -0
- data/docs/customization/javascript.md +161 -0
- data/docs/customization/theming-rubyui.md +80 -0
- data/docs/dashboards/overview.md +65 -0
- data/docs/fields/overview.md +94 -0
- data/docs/filters/overview.md +73 -0
- data/docs/generators-and-tasks/overview.md +42 -0
- data/docs/getting-started/authentication.md +98 -0
- data/docs/getting-started/configuration.md +174 -0
- data/docs/getting-started/installation.md +201 -0
- data/docs/getting-started/internationalization.md +50 -0
- data/docs/getting-started/practical-guide.md +310 -0
- data/docs/resources/associations.md +66 -0
- data/docs/resources/index-customization.md +68 -0
- data/docs/resources/overview.md +50 -0
- data/docs/resources/scopes.md +54 -0
- data/docs/resources/tabs-panels.md +65 -0
- data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
- data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
- data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
- data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
- data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
- data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
- data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
- data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
- data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
- data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
- data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
- data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
- data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
- data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
- data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
- data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
- data/lib/ruby_ui_admin/authorization.rb +40 -0
- data/lib/ruby_ui_admin/base_action.rb +211 -0
- data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
- data/lib/ruby_ui_admin/base_policy.rb +38 -0
- data/lib/ruby_ui_admin/base_resource.rb +503 -0
- data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
- data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
- data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
- data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
- data/lib/ruby_ui_admin/configuration.rb +177 -0
- data/lib/ruby_ui_admin/current.rb +20 -0
- data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
- data/lib/ruby_ui_admin/engine.rb +80 -0
- data/lib/ruby_ui_admin/execution_context.rb +36 -0
- data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
- data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
- data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
- data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
- data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
- data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
- data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
- data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
- data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
- data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
- data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
- data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
- data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
- data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
- data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
- data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
- data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
- data/lib/ruby_ui_admin/menu/builder.rb +94 -0
- data/lib/ruby_ui_admin/resource_manager.rb +82 -0
- data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
- data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
- data/lib/ruby_ui_admin/structure.rb +40 -0
- data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
- data/lib/ruby_ui_admin/version.rb +5 -0
- data/lib/ruby_ui_admin/view.rb +34 -0
- data/lib/ruby_ui_admin.rb +110 -0
- data/lib/tasks/ruby_ui_admin.rake +71 -0
- data/public/ruby-ui-admin-assets/application.css +2 -0
- data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
- data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
- data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
- data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
- data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
- data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
- data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
- metadata +379 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
namespace "ruby_ui_admin:install"
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Creates the RubyUI Admin initializer and mounts the engine."
|
|
12
|
+
|
|
13
|
+
class_option :path, type: :string, default: "admin",
|
|
14
|
+
desc: "Path to mount the admin at (default: admin)"
|
|
15
|
+
|
|
16
|
+
def create_initializer
|
|
17
|
+
template "initializer.rb.tt", "config/initializers/ruby_ui_admin.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def mount_engine
|
|
21
|
+
route %(mount_ruby_ui_admin at: "/#{options[:path]}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def done
|
|
25
|
+
say "\nRubyUI Admin: initializer created and engine mounted.", :green
|
|
26
|
+
warn_if_ruby_ui_missing
|
|
27
|
+
say "\nThe admin renders your app's RubyUI components + assets, so finish wiring them:", :yellow
|
|
28
|
+
say " 1. Install RubyUI and generate its components (if you haven't). Run phlex:install"
|
|
29
|
+
say " FIRST — RubyUI's installer skips it when phlex-rails is present (it is, via this"
|
|
30
|
+
say " gem) and then aborts on a missing app/components/base.rb:"
|
|
31
|
+
say " bin/rails generate phlex:install"
|
|
32
|
+
say " bin/rails generate ruby_ui:install"
|
|
33
|
+
say " Then generate the components the admin needs (drives ruby_ui:component for you):"
|
|
34
|
+
say " bin/rails generate ruby_ui_admin:components"
|
|
35
|
+
say " 2. Tailwind: extract the admin's classes into your app and @source the result:"
|
|
36
|
+
say " bin/rails ruby_ui_admin:tailwind_source # writes app/assets/tailwind/ruby_ui_admin_classes.html"
|
|
37
|
+
say %( @source "./ruby_ui_admin_classes.html"; # in your Tailwind entry CSS (commit the file))
|
|
38
|
+
say " 3. Stimulus: copy the admin's controllers into your app with"
|
|
39
|
+
say " bin/rails generate ruby_ui_admin:assets"
|
|
40
|
+
say " then add the one import line it prints to app/javascript/application.js."
|
|
41
|
+
say " 4. Set config.head_assets in config/initializers/ruby_ui_admin.rb (see the comment there)."
|
|
42
|
+
say "\nFull guide: docs/getting-started/installation.md", :green
|
|
43
|
+
say "Then generate your first resource:"
|
|
44
|
+
say " bin/rails generate ruby_ui_admin:resource <Model>\n"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# RubyUI's installer aborts (leaving RubyUI::Base ungenerated) if it runs before
|
|
50
|
+
# phlex:install on an app that already has phlex-rails — which every ruby_ui_admin app does.
|
|
51
|
+
# Surface that here so a half-installed RubyUI doesn't show up later as a cryptic Zeitwerk error.
|
|
52
|
+
def warn_if_ruby_ui_missing
|
|
53
|
+
return if File.exist?(Rails.root.join("app/components/ruby_ui/base.rb"))
|
|
54
|
+
|
|
55
|
+
say "\n⚠️ RubyUI doesn't look fully installed (app/components/ruby_ui/base.rb is missing).", :red
|
|
56
|
+
say " RubyUI's `ruby_ui:install` aborts on a missing app/components/base.rb when phlex-rails", :red
|
|
57
|
+
say " is already present (it is, via this gem). Run `bin/rails generate phlex:install` first,", :red
|
|
58
|
+
say " then re-run `bin/rails generate ruby_ui:install`.", :red
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RubyUIAdmin.configure do |config|
|
|
4
|
+
# The name shown in the sidebar and page titles.
|
|
5
|
+
config.app_name = "<%= Rails.application.class.module_parent_name %> Admin"
|
|
6
|
+
|
|
7
|
+
# Where the engine is mounted (keep in sync with config/routes.rb).
|
|
8
|
+
config.root_path = "/<%= options[:path] %>"
|
|
9
|
+
|
|
10
|
+
# Records per page on index views.
|
|
11
|
+
config.per_page = 24
|
|
12
|
+
|
|
13
|
+
# Resolve the signed-in admin. The block runs in the controller context.
|
|
14
|
+
config.current_user_method { current_user }
|
|
15
|
+
|
|
16
|
+
# Optional gate run before every admin request, e.g.:
|
|
17
|
+
# config.authenticate_with { redirect_to main_app.login_path unless current_user&.admin? }
|
|
18
|
+
|
|
19
|
+
# Authorization adapter: :action_policy (default), :pundit, :cancancan, or a custom adapter
|
|
20
|
+
# class. Set to nil to disable. See docs/authorization/adapters.md.
|
|
21
|
+
config.authorization_client = :action_policy
|
|
22
|
+
|
|
23
|
+
# When true, rules a policy doesn't define (and missing policies) are denied.
|
|
24
|
+
config.explicit_authorization = false
|
|
25
|
+
|
|
26
|
+
# The admin renders your app's RubyUI components, so it uses your app's CSS + JS. Point the
|
|
27
|
+
# layout's <head> at them here (a proc evaluated in the Rails view context). Make sure your
|
|
28
|
+
# Tailwind build covers app/components/ruby_ui/** and the admin views, and that your bundler
|
|
29
|
+
# registers RubyUI's Stimulus controllers + the admin's rua--* controllers.
|
|
30
|
+
# config.head_assets = lambda do
|
|
31
|
+
# safe_join([
|
|
32
|
+
# stylesheet_link_tag("application", "data-turbo-track": "reload"),
|
|
33
|
+
# javascript_importmap_tags
|
|
34
|
+
# ])
|
|
35
|
+
# end
|
|
36
|
+
|
|
37
|
+
# In-app docs browser at <mount>/docs (renders the gem's docs/*.md). Defaults to :local
|
|
38
|
+
# (development/test only, open). Set to true to expose it in production too (behind your auth
|
|
39
|
+
# gate), or false to disable. Needs kramdown + kramdown-parser-gfm in the target environment.
|
|
40
|
+
# config.docs_enabled = :local
|
|
41
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
module Generators
|
|
7
|
+
# Copies the bundled locale files into the host app so they can be edited/extended.
|
|
8
|
+
class LocalesGenerator < Rails::Generators::Base
|
|
9
|
+
namespace "ruby_ui_admin:locales"
|
|
10
|
+
source_root RubyUIAdmin::Engine.root.to_s
|
|
11
|
+
|
|
12
|
+
desc "Copies RubyUI Admin's locale files into config/locales."
|
|
13
|
+
|
|
14
|
+
def copy_locales
|
|
15
|
+
Dir[File.join(self.class.source_root, "config", "locales", "*.yml")].each do |file|
|
|
16
|
+
relative = File.join("config", "locales", File.basename(file))
|
|
17
|
+
copy_file relative, relative
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
module Generators
|
|
7
|
+
class PolicyGenerator < Rails::Generators::NamedBase
|
|
8
|
+
namespace "ruby_ui_admin:policy"
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Generates a RubyUI Admin action_policy policy."
|
|
12
|
+
|
|
13
|
+
def create_policy
|
|
14
|
+
template "policy.rb.tt", File.join("app/ruby_ui_admin/policies", "#{file_path}_policy.rb")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Policies
|
|
5
|
+
class <%= class_name %>Policy < RubyUIAdmin::BasePolicy
|
|
6
|
+
def index? = true
|
|
7
|
+
|
|
8
|
+
def show? = true
|
|
9
|
+
|
|
10
|
+
def create? = true
|
|
11
|
+
|
|
12
|
+
def update? = true
|
|
13
|
+
|
|
14
|
+
def destroy? = true
|
|
15
|
+
|
|
16
|
+
def act_on? = true
|
|
17
|
+
|
|
18
|
+
# Scope records on the index view:
|
|
19
|
+
# relation_scope do |relation|
|
|
20
|
+
# relation
|
|
21
|
+
# end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
module Generators
|
|
7
|
+
class ResourceGenerator < Rails::Generators::NamedBase
|
|
8
|
+
namespace "ruby_ui_admin:resource"
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Generates a RubyUI Admin resource (with fields derived from the model)."
|
|
12
|
+
|
|
13
|
+
class_option :model_class, type: :string, desc: "Override the inferred model class name"
|
|
14
|
+
|
|
15
|
+
def create_resource
|
|
16
|
+
template "resource.rb.tt", File.join("app/ruby_ui_admin/resources", "#{singular_name}.rb")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def resource_class_name
|
|
22
|
+
class_name.demodulize
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def model_class_name
|
|
26
|
+
options[:model_class]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def model_klass
|
|
30
|
+
(model_class_name || class_name).safe_constantize
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Builds the body of `def fields`, one `field ...` line per column/association.
|
|
34
|
+
def fields_block
|
|
35
|
+
indent = " "
|
|
36
|
+
lines = ["#{indent}field :id, as: :id"]
|
|
37
|
+
|
|
38
|
+
if (model = model_klass)
|
|
39
|
+
model.columns.each do |column|
|
|
40
|
+
next if column.name == "id" || column.name.end_with?("_id")
|
|
41
|
+
|
|
42
|
+
type = column_field_type(column)
|
|
43
|
+
# Keep the index lean: long-form fields (textarea/code) are noisy in a table, so show
|
|
44
|
+
# them only on the record's show/new/edit pages by default. Tweak or drop `only_on:`.
|
|
45
|
+
suffix = hidden_from_index?(type) ? ", only_on: %i[show new edit]" : ""
|
|
46
|
+
lines << "#{indent}field :#{column.name}, as: :#{type}#{suffix}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
model.reflect_on_all_associations.each do |association|
|
|
50
|
+
type = association_field_type(association)
|
|
51
|
+
lines << "#{indent}field :#{association.name}, as: :#{type}" if type
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
lines.join("\n")
|
|
56
|
+
rescue
|
|
57
|
+
" field :id, as: :id"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def column_field_type(column)
|
|
61
|
+
return :password if column.name.match?(/password/)
|
|
62
|
+
return :text if column.name == "email"
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
string: :text, text: :textarea, integer: :number, float: :number,
|
|
66
|
+
decimal: :number, boolean: :boolean, date: :date, datetime: :date_time,
|
|
67
|
+
timestamp: :date_time, json: :code, jsonb: :code
|
|
68
|
+
}.fetch(column.type, :text)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Long-form field types are hidden from the index by default (still on show/new/edit).
|
|
72
|
+
def hidden_from_index?(type)
|
|
73
|
+
%i[textarea code].include?(type)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def association_field_type(association)
|
|
77
|
+
{
|
|
78
|
+
belongs_to: :belongs_to, has_one: :has_one,
|
|
79
|
+
has_many: :has_many, has_and_belongs_to_many: :has_and_belongs_to_many
|
|
80
|
+
}[association.macro]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Resources
|
|
5
|
+
class <%= resource_class_name %> < RubyUIAdmin::BaseResource
|
|
6
|
+
<% if model_class_name -%>
|
|
7
|
+
self.model_class = <%= model_class_name %>
|
|
8
|
+
|
|
9
|
+
<% end -%>
|
|
10
|
+
def fields
|
|
11
|
+
<%= fields_block %>
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
module Generators
|
|
7
|
+
class ScopeGenerator < Rails::Generators::NamedBase
|
|
8
|
+
namespace "ruby_ui_admin:scope"
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Generates a RubyUI Admin named index scope."
|
|
12
|
+
|
|
13
|
+
def create_scope
|
|
14
|
+
template "scope.rb.tt", File.join("app/ruby_ui_admin/scopes", "#{file_path}.rb")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_policy"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
module Authorization
|
|
7
|
+
# action_policy-backed adapter (the default). Bridges the admin's authorization onto
|
|
8
|
+
# action_policy: `allowed_to?`, `authorized_scope`, `policy_for`. Rules the policy doesn't
|
|
9
|
+
# define fall back to its default rule (`manage?`), which `RubyUIAdmin::BasePolicy` ties to
|
|
10
|
+
# `explicit_authorization`; a missing policy is decided by `explicit_authorization` too.
|
|
11
|
+
class ActionPolicyAdapter < Adapter
|
|
12
|
+
include ActionPolicy::Behaviour
|
|
13
|
+
|
|
14
|
+
# Declares the authorization context required by policies (resolved via the readers on Adapter).
|
|
15
|
+
authorize :user
|
|
16
|
+
authorize :true_user
|
|
17
|
+
|
|
18
|
+
def authorize_action(rule, record: nil, raise_exception: true)
|
|
19
|
+
return true unless RubyUIAdmin.configuration.authorization_enabled?
|
|
20
|
+
|
|
21
|
+
target = record.nil? ? @record : record
|
|
22
|
+
normalized = normalize_rule(rule)
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
result = allowed_to?(normalized, target, with: policy_class)
|
|
26
|
+
rescue ActionPolicy::NotFound
|
|
27
|
+
return handle_missing_policy(raise_exception)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if !result && raise_exception
|
|
31
|
+
raise NotAuthorizedError, "Not authorized to #{normalized} on #{target.inspect}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def apply_policy(scope)
|
|
38
|
+
return scope unless RubyUIAdmin.configuration.authorization_enabled?
|
|
39
|
+
|
|
40
|
+
authorized_scope(scope, with: policy_class)
|
|
41
|
+
rescue ActionPolicy::UnknownScopeType,
|
|
42
|
+
ActionPolicy::UnknownNamedScope,
|
|
43
|
+
ActionPolicy::UnrecognizedScopeTarget,
|
|
44
|
+
ActionPolicy::NotFound
|
|
45
|
+
scope
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def has_rule?(rule, record: nil)
|
|
49
|
+
target = record.nil? ? @record : record
|
|
50
|
+
policy = policy_for(record: target, with: policy_class, allow_nil: true)
|
|
51
|
+
return false if policy.nil?
|
|
52
|
+
|
|
53
|
+
policy.respond_to?(normalize_rule(rule))
|
|
54
|
+
rescue ActionPolicy::NotFound
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Whether the policy CLASS explicitly defines this rule as a public method (i.e. not satisfied
|
|
59
|
+
# only by the `manage?` default). Used for field-level authorization.
|
|
60
|
+
def defines_rule?(rule)
|
|
61
|
+
return false if policy_class.nil?
|
|
62
|
+
|
|
63
|
+
policy_class.public_method_defined?(normalize_rule(rule))
|
|
64
|
+
rescue
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Authorization
|
|
5
|
+
# Interface every authorization adapter implements. `Services::AuthorizationService` resolves the
|
|
6
|
+
# configured adapter (`config.authorization_client`) and delegates to it, so call sites stay
|
|
7
|
+
# backend-agnostic. An adapter bridges the admin's rule vocabulary (`index?`/`show?`/`update?`/
|
|
8
|
+
# `destroy?`/`act_on?`/`view_<field>?`, with the `manage?` + `explicit_authorization` fallback)
|
|
9
|
+
# onto a backend (action_policy, Pundit, CanCanCan, …).
|
|
10
|
+
#
|
|
11
|
+
# Subclasses must implement: #authorize_action, #apply_policy, #has_rule?, #defines_rule?.
|
|
12
|
+
# The shared context (#user, #true_user, #record, #policy_class), #allowed? and #normalize_rule
|
|
13
|
+
# live here.
|
|
14
|
+
class Adapter
|
|
15
|
+
attr_reader :user, :record, :policy_class
|
|
16
|
+
|
|
17
|
+
def initialize(user, record = nil, policy_class: nil, true_user: nil)
|
|
18
|
+
@user = user
|
|
19
|
+
@record = record
|
|
20
|
+
@policy_class = policy_class
|
|
21
|
+
@true_user = true_user
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The real (impersonating) user. Defaults to `Current.true_user`, falling back to `user`.
|
|
25
|
+
def true_user
|
|
26
|
+
@true_user || RubyUIAdmin::Current.true_user || @user
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_record(record)
|
|
30
|
+
@record = record
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Authorizes a single rule. Returns a boolean; raises `NotAuthorizedError` when denied and
|
|
35
|
+
# `raise_exception` is true.
|
|
36
|
+
def authorize_action(_rule, record: nil, raise_exception: true)
|
|
37
|
+
raise NotImplementedError, "#{self.class} must implement #authorize_action"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def allowed?(rule, record: nil)
|
|
41
|
+
authorize_action(rule, record: record, raise_exception: false)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Applies the policy's record scope to a relation; returns it unchanged when none applies.
|
|
45
|
+
def apply_policy(_scope)
|
|
46
|
+
raise NotImplementedError, "#{self.class} must implement #apply_policy"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Whether the resolved policy/instance responds to the rule (for the given record).
|
|
50
|
+
def has_rule?(_rule, record: nil)
|
|
51
|
+
raise NotImplementedError, "#{self.class} must implement #has_rule?"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Whether the policy CLASS explicitly defines the rule (field-level auth: an undefined rule
|
|
55
|
+
# means "no opinion" → the field stays visible).
|
|
56
|
+
def defines_rule?(_rule)
|
|
57
|
+
raise NotImplementedError, "#{self.class} must implement #defines_rule?"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def normalize_rule(rule)
|
|
63
|
+
string = rule.to_s
|
|
64
|
+
string.end_with?("?") ? string.to_sym : :"#{string}?"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Shared "no policy / undefined rule" fallback: `explicit_authorization` decides — denied when
|
|
68
|
+
# true (raising if requested), allowed when false. Used by every adapter so the
|
|
69
|
+
# missing-policy/undefined-rule semantics stay identical across backends.
|
|
70
|
+
def handle_missing_policy(raise_exception)
|
|
71
|
+
if RubyUIAdmin.configuration.explicit_authorization
|
|
72
|
+
raise NotAuthorizedError, "No policy found" if raise_exception
|
|
73
|
+
false
|
|
74
|
+
else
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "cancancan"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise LoadError, "config.authorization_client = :cancancan requires the `cancancan` gem. Add `gem \"cancancan\"` to your Gemfile."
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module RubyUIAdmin
|
|
10
|
+
module Authorization
|
|
11
|
+
# CanCanCan-backed adapter. Maps the admin's CRUD rules onto `Ability#can?(action, subject)` and
|
|
12
|
+
# the index scope onto `relation.accessible_by(ability)`.
|
|
13
|
+
#
|
|
14
|
+
# ⚠️ PARTIAL by design (CanCanCan is action/subject-based, not policy-class-based):
|
|
15
|
+
# * No `policy_class` — a single global Ability (`config.cancancan_ability_class`, default
|
|
16
|
+
# `Ability`) decides everything; `authorization_policy` per resource is ignored.
|
|
17
|
+
# * **No field-level authorization** — CanCanCan has no per-rule "is it defined?" concept, so
|
|
18
|
+
# `defines_rule?`/`has_rule?` return false (every field stays visible). Use action_policy or
|
|
19
|
+
# Pundit if you need field-level rules.
|
|
20
|
+
# * **`explicit_authorization` has no effect** — CanCanCan denies whatever the Ability doesn't
|
|
21
|
+
# grant; there's no `manage?`/undefined-rule fallback. Grant access in your Ability.
|
|
22
|
+
# * `true_user` (impersonation) is not passed to the Ability.
|
|
23
|
+
# * The index scope follows the Ability's rules for the action (e.g. `can :read, Post, ...`);
|
|
24
|
+
# with no matching rule it resolves to an empty relation, the CanCanCan way.
|
|
25
|
+
class CanCanCanAdapter < Adapter
|
|
26
|
+
def authorize_action(rule, record: nil, raise_exception: true)
|
|
27
|
+
return true unless RubyUIAdmin.configuration.authorization_enabled?
|
|
28
|
+
|
|
29
|
+
target = record.nil? ? @record : record
|
|
30
|
+
action = cancan_action(rule)
|
|
31
|
+
result = ability.can?(action, target)
|
|
32
|
+
|
|
33
|
+
if !result && raise_exception
|
|
34
|
+
raise NotAuthorizedError, "Not authorized to #{action} on #{target.inspect}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def apply_policy(scope)
|
|
41
|
+
return scope unless RubyUIAdmin.configuration.authorization_enabled?
|
|
42
|
+
|
|
43
|
+
scope.accessible_by(ability)
|
|
44
|
+
rescue CanCan::Error
|
|
45
|
+
scope
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Field-level authorization is not supported by CanCanCan (no per-rule definition concept).
|
|
49
|
+
def has_rule?(_rule, record: nil) = false
|
|
50
|
+
|
|
51
|
+
def defines_rule?(_rule) = false
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def ability
|
|
56
|
+
@ability ||= ability_class.new(user)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ability_class
|
|
60
|
+
configured = RubyUIAdmin.configuration.cancancan_ability_class || "Ability"
|
|
61
|
+
configured.is_a?(Class) ? configured : Object.const_get(configured.to_s)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Admin rule (`show?`, `update?`, …) → CanCanCan action (`:show`, `:update`, …).
|
|
65
|
+
def cancan_action(rule)
|
|
66
|
+
rule.to_s.delete_suffix("?").to_sym
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "pundit"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise LoadError, "config.authorization_client = :pundit requires the `pundit` gem. Add `gem \"pundit\"` to your Gemfile."
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module RubyUIAdmin
|
|
10
|
+
module Authorization
|
|
11
|
+
# Pundit-backed adapter. Maps the admin's rules onto a Pundit policy (`policy.show?`), record
|
|
12
|
+
# scopes onto the policy's `Scope`, and uses `respond_to?`/`public_method_defined?` for the
|
|
13
|
+
# field-level "is this rule defined?" check. A rule the policy doesn't define (or a missing
|
|
14
|
+
# policy) falls back to `explicit_authorization`, keeping the same semantics as action_policy.
|
|
15
|
+
#
|
|
16
|
+
# The resource's `authorization_policy` is used as the policy class when given; otherwise Pundit
|
|
17
|
+
# infers it (`Pundit::PolicyFinder`). Pundit policies receive only `user` — `true_user`
|
|
18
|
+
# (impersonation) is not passed to them.
|
|
19
|
+
class PunditAdapter < Adapter
|
|
20
|
+
def authorize_action(rule, record: nil, raise_exception: true)
|
|
21
|
+
return true unless RubyUIAdmin.configuration.authorization_enabled?
|
|
22
|
+
|
|
23
|
+
target = record.nil? ? @record : record
|
|
24
|
+
policy = policy_instance(target)
|
|
25
|
+
normalized = normalize_rule(rule)
|
|
26
|
+
|
|
27
|
+
return handle_missing_policy(raise_exception) if policy.nil? || !policy.respond_to?(normalized)
|
|
28
|
+
|
|
29
|
+
result = !!policy.public_send(normalized)
|
|
30
|
+
if !result && raise_exception
|
|
31
|
+
raise NotAuthorizedError, "Not authorized to #{normalized} on #{target.inspect}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def apply_policy(scope)
|
|
38
|
+
return scope unless RubyUIAdmin.configuration.authorization_enabled?
|
|
39
|
+
|
|
40
|
+
scope_class = scope_class_for(scope)
|
|
41
|
+
return scope unless scope_class
|
|
42
|
+
|
|
43
|
+
scope_class.new(user, scope).resolve
|
|
44
|
+
rescue Pundit::NotDefinedError
|
|
45
|
+
scope
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def has_rule?(rule, record: nil)
|
|
49
|
+
target = record.nil? ? @record : record
|
|
50
|
+
policy = policy_instance(target)
|
|
51
|
+
return false if policy.nil?
|
|
52
|
+
|
|
53
|
+
policy.respond_to?(normalize_rule(rule))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def defines_rule?(rule)
|
|
57
|
+
return false if policy_class.nil?
|
|
58
|
+
|
|
59
|
+
policy_class.public_method_defined?(normalize_rule(rule))
|
|
60
|
+
rescue
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Explicit `authorization_policy` wins; otherwise Pundit infers the policy class from the
|
|
67
|
+
# target (instance or class). Returns nil when none can be resolved.
|
|
68
|
+
def policy_instance(target)
|
|
69
|
+
klass = policy_class || Pundit::PolicyFinder.new(target).policy
|
|
70
|
+
return nil unless klass
|
|
71
|
+
|
|
72
|
+
klass.new(user, target)
|
|
73
|
+
rescue Pundit::NotDefinedError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def scope_class_for(scope)
|
|
78
|
+
if policy_class&.const_defined?(:Scope)
|
|
79
|
+
policy_class::Scope
|
|
80
|
+
else
|
|
81
|
+
Pundit::PolicyFinder.new(scope).scope
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
# Resolves the authorization adapter from `config.authorization_client`. The value may be a
|
|
5
|
+
# symbol (looked up in REGISTRY), a Class (a custom adapter), or nil (disabled — still resolves
|
|
6
|
+
# to the default adapter, which short-circuits to permissive via `authorization_enabled?`).
|
|
7
|
+
module Authorization
|
|
8
|
+
# Symbol → adapter class, wrapped in a lambda so optional backends (Pundit, CanCanCan) are only
|
|
9
|
+
# referenced when actually selected.
|
|
10
|
+
REGISTRY = {
|
|
11
|
+
action_policy: -> { ActionPolicyAdapter },
|
|
12
|
+
pundit: lambda {
|
|
13
|
+
require "ruby_ui_admin/authorization/pundit_adapter"
|
|
14
|
+
PunditAdapter
|
|
15
|
+
},
|
|
16
|
+
cancancan: lambda {
|
|
17
|
+
require "ruby_ui_admin/authorization/can_can_can_adapter"
|
|
18
|
+
CanCanCanAdapter
|
|
19
|
+
}
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def self.adapter_class(client = RubyUIAdmin.configuration.authorization_client)
|
|
23
|
+
return client if client.is_a?(Class)
|
|
24
|
+
|
|
25
|
+
key = (client || :action_policy).to_sym
|
|
26
|
+
resolver = REGISTRY[key]
|
|
27
|
+
unless resolver
|
|
28
|
+
known = REGISTRY.keys.join(", ")
|
|
29
|
+
raise ArgumentError,
|
|
30
|
+
"Unknown authorization_client #{client.inspect}. Use one of: #{known}, an adapter class, or nil."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
resolver.call
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.build(...)
|
|
37
|
+
adapter_class.new(...)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|