active_element 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +8 -9
  5. data/active_element.gemspec +1 -1
  6. data/app/assets/javascripts/active_element/application.js +3 -1
  7. data/app/assets/javascripts/active_element/popover.js +6 -0
  8. data/app/assets/javascripts/active_element/setup.js +12 -0
  9. data/app/assets/javascripts/active_element/{search_field.js → text_search_field.js} +2 -2
  10. data/app/assets/javascripts/active_element/toast.js +8 -0
  11. data/app/assets/stylesheets/active_element/application.scss +65 -1
  12. data/app/controllers/active_element/application_controller.rb +13 -19
  13. data/app/views/active_element/components/form/_label.html.erb +2 -2
  14. data/app/views/active_element/components/form/_templates.html.erb +8 -8
  15. data/app/views/active_element/components/form.html.erb +3 -3
  16. data/app/views/active_element/components/table/_collection_row.html.erb +3 -3
  17. data/app/views/active_element/components/table/collection.html.erb +1 -1
  18. data/app/views/active_element/components/table/item.html.erb +3 -3
  19. data/app/views/active_element/navbar/_menu.html.erb +2 -2
  20. data/app/views/layouts/active_element.html.erb +21 -23
  21. data/app/views/layouts/active_element_error.html.erb +1 -1
  22. data/config/routes.rb +10 -1
  23. data/lib/active_element/components/button.rb +6 -41
  24. data/lib/active_element/components/form.rb +12 -3
  25. data/lib/active_element/components/text_search/active_record_authorization.rb +13 -0
  26. data/lib/active_element/components/text_search/authorization.rb +117 -0
  27. data/lib/active_element/components/text_search/component.rb +118 -0
  28. data/lib/active_element/components/text_search/sql.rb +107 -0
  29. data/lib/active_element/components/text_search.rb +23 -0
  30. data/lib/active_element/components/util/decorator.rb +2 -2
  31. data/lib/active_element/components/util/record_path.rb +84 -0
  32. data/lib/active_element/components/util.rb +7 -2
  33. data/lib/active_element/components.rb +1 -0
  34. data/lib/active_element/controller_action.rb +9 -10
  35. data/lib/active_element/controller_interface.rb +78 -0
  36. data/lib/active_element/permissions_check.rb +33 -29
  37. data/lib/active_element/permissions_report.rb +57 -0
  38. data/lib/active_element/route.rb +1 -1
  39. data/lib/active_element/routes.rb +1 -1
  40. data/lib/active_element/version.rb +1 -1
  41. data/lib/active_element.rb +24 -10
  42. data/lib/tasks/active_element.rake +1 -16
  43. data/rspec-documentation/pages/Components/Tables.md +2 -2
  44. data/rspec-documentation/spec_helper.rb +1 -1
  45. metadata +19 -12
  46. data/app/controllers/active_element/text_searches_controller.rb +0 -189
  47. data/lib/active_element/active_record_text_search_authorization.rb +0 -12
  48. data/lib/active_element/colorized_string.rb +0 -33
@@ -22,6 +22,7 @@ module ActiveElement
22
22
  @modal = modal
23
23
  @kwargs = kwargs
24
24
  @columns = columns
25
+ @action = kwargs.delete(:action) { default_action }
25
26
  @method = kwargs.delete(:method) { default_method }.to_s.downcase.to_sym
26
27
  end
27
28
  # rubocop:enable Metrics/MethodLength
@@ -30,7 +31,7 @@ module ActiveElement
30
31
  'active_element/components/form'
31
32
  end
32
33
 
33
- def locals # rubocop:disable Metrics/MethodLength
34
+ def locals # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
34
35
  {
35
36
  component: self,
36
37
  fields: Util::FormFieldMapping.new(record, fields, i18n).fields_with_types_and_options,
@@ -39,6 +40,7 @@ module ActiveElement
39
40
  submit_position: submit_position,
40
41
  class_name: class_name,
41
42
  method: method,
43
+ action: action,
42
44
  kwargs: kwargs,
43
45
  destroy: destroy,
44
46
  modal: modal,
@@ -130,7 +132,8 @@ module ActiveElement
130
132
 
131
133
  private
132
134
 
133
- attr_reader :fields, :submit, :title, :kwargs, :item, :method, :destroy, :modal, :expanded, :columns
135
+ attr_reader :fields, :submit, :title, :kwargs, :item, :method, :action,
136
+ :destroy, :modal, :expanded, :columns
134
137
 
135
138
  def valid_field?(field)
136
139
  return true if record.respond_to?("#{field}_changed?") && !record.public_send("#{field}_changed?")
@@ -197,7 +200,7 @@ module ActiveElement
197
200
 
198
201
  def default_method
199
202
  case controller.action_name
200
- when 'edit'
203
+ when 'edit', 'update'
201
204
  'PATCH'
202
205
  when 'index'
203
206
  'GET'
@@ -205,6 +208,12 @@ module ActiveElement
205
208
  'POST'
206
209
  end
207
210
  end
211
+
212
+ def default_action
213
+ return controller.request.path unless record.is_a?(ActiveModel::Naming)
214
+
215
+ Util::RecordPath.new(record: record, controller: controller).path
216
+ end
208
217
  end
209
218
  end
210
219
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::Base.class_eval do
4
+ class << self
5
+ def authorize_active_element_text_search(with:, providing:)
6
+ ActiveElement::Components::TextSearch.register_authorized_text_search(
7
+ model: self,
8
+ with: with,
9
+ providing: providing
10
+ )
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ module TextSearch
6
+ # Manages authorization for text search, ensures model is configured for text search and
7
+ # that user has correct permissions.
8
+ class Authorization
9
+ include Paintbrush
10
+
11
+ class << self
12
+ def permission_for(model:, field:)
13
+ "can_text_search_#{application_name}_#{model.name.underscore.pluralize}_with_#{field}"
14
+ end
15
+
16
+ private
17
+
18
+ def application_name
19
+ RailsComponent.new(::Rails).application_name
20
+ end
21
+ end
22
+
23
+ def initialize(model:, params:, user:, search_columns:, result_columns:)
24
+ @model = model
25
+ @params = params
26
+ @user = user
27
+ @search_columns = search_columns
28
+ @result_columns = result_columns
29
+ end
30
+
31
+ def authorized?
32
+ return true if authorized_model? && authorized_user?
33
+ return false.tap { ActiveElement.warning(message) } unless Rails.env.development?
34
+
35
+ ActiveElement.warning(development_message)
36
+ true
37
+ end
38
+
39
+ def message(colorize: true)
40
+ [
41
+ missing_authorization_message(colorize: colorize),
42
+ missing_permissions_message(colorize: colorize)
43
+ ].compact.join(paintbrush(colorize: colorize) { red '. ' })
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :model, :params, :user, :search_columns, :result_columns
49
+
50
+ def development_message
51
+ paintbrush { green "Bypassed text search authorization in development environment: #{yellow message}" }
52
+ end
53
+
54
+ def missing_permissions
55
+ (search_columns + result_columns).reject { |column| user_permitted?(column) }
56
+ .map { |column| permission_for(column.name) }
57
+ .uniq
58
+ .sort
59
+ end
60
+
61
+ def missing_permissions_message(colorize:)
62
+ return nil if missing_permissions.empty?
63
+
64
+ paintbrush(colorize: colorize) { red "Missing permissions: #{yellow missing_permissions.join(', ')}" }
65
+ end
66
+
67
+ def missing_authorization_message(colorize:)
68
+ return nil if authorized_model?
69
+
70
+ paintbrush(colorize: colorize) do
71
+ red "Missing model authorization for #{cyan model_name} with: " \
72
+ "#{green search_fields.join(', ')}, providing: " \
73
+ "#{green result_fields.join(', ')}"
74
+ end
75
+ end
76
+
77
+ def search_fields
78
+ params[:attributes]
79
+ end
80
+
81
+ def model_name
82
+ model&.name || params[:model]
83
+ end
84
+
85
+ def result_fields
86
+ (params[:attributes] + [params[:value]]).uniq
87
+ end
88
+
89
+ def user_permitted?(column)
90
+ user&.permissions&.include?(permission_for(column.name))
91
+ end
92
+
93
+ def authorized_user?
94
+ missing_permissions.empty?
95
+ end
96
+
97
+ def permission_for(field)
98
+ self.class.permission_for(model: model, field: field)
99
+ end
100
+
101
+ def authorized_model?
102
+ TextSearch.authorized_text_searches.any? do |authorized_model, search_fields, result_fields|
103
+ next false unless authorized_model == model
104
+ next false unless authorized_fields?(search_columns, Array(search_fields))
105
+ next false unless authorized_fields?(result_columns, Array(result_fields))
106
+
107
+ true
108
+ end
109
+ end
110
+
111
+ def authorized_fields?(columns, fields)
112
+ columns.all? { |column| fields.map(&:to_sym).include?(column.name.to_sym) }
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ module TextSearch
6
+ # Used by auto-complete search field for executing a text search on the provided model and
7
+ # attributes.
8
+ #
9
+ # The user must have a permission configured for each field used in the search:
10
+ # `can_text_search_<application_name>_<models>_with_<field>`
11
+ #
12
+ # A model must call `authorize_active_element_text_search` to enable text search. e.g.:
13
+ #
14
+ # class MyModel < ApplicationRecord
15
+ # authorize_active_element_text_search with: [:id, :email],
16
+ # providing: [:id, :first_name, :last_name, :email]
17
+ # end
18
+ #
19
+ # This allows searching using the `name` `email` columns and permits returning each matching
20
+ # record's `id`, `first_name`, `last_name`, and `email` values.
21
+ #
22
+ # This complexity exists to ensure that authenticated users can only retrieve specific
23
+ # database values that are explicitly configured, as well as ensuring that users cannot
24
+ # search arbitrary columns. Requiring this logic in the model is intended to reduce
25
+ # likelihood of DoS vulnerabilities if users are able to search unindexed columns.
26
+ #
27
+ # Note that the `/_active_element_text_search` endpoint added to each controller
28
+ # necessarily receives arbitrary arguments. Configuring a form to only fetch certain values
29
+ # does not restrict potential parameters, so a strict permissions and model configuration
30
+ # system is required to govern access to database queries.
31
+ #
32
+ class Component
33
+ DEFAULT_LIMIT = 50
34
+
35
+ def initialize(controller:)
36
+ @controller = controller
37
+ @params = controller.params
38
+ end
39
+
40
+ def response
41
+ return unverified_parameters unless verified_parameters?
42
+ return unverified_model unless verified_model?
43
+ return unauthorized unless authorization.authorized?
44
+
45
+ { json: { results: results, request_id: controller.params[:request_id] }, status: :created }
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :controller, :params
51
+
52
+ def verified_parameters?
53
+ %i[model attributes value query].all? { |parameter| params[parameter].present? }
54
+ end
55
+
56
+ def unverified_parameters
57
+ { json: { message: 'Must provide parameters: [model, attributes, value, query] for text search.' },
58
+ status: :unprocessable_entity }
59
+ end
60
+
61
+ def verified_model?
62
+ [model, sql.search_columns, sql.value_column].all?(&:present?)
63
+ end
64
+
65
+ def unverified_model
66
+ { json: { message: authorization.message(colorize: false) }, status: :unprocessable_entity }
67
+ end
68
+
69
+ def unauthorized
70
+ { json: { message: authorization.message(colorize: false) }, status: :forbidden }
71
+ end
72
+
73
+ def sql
74
+ @sql ||= Sql.new(
75
+ model: model,
76
+ query: params[:query],
77
+ value: params[:value],
78
+ attributes: params[:attributes]
79
+ )
80
+ end
81
+
82
+ def results
83
+ @results ||= model.where(*sql.whereclause)
84
+ .limit(limit)
85
+ .pluck(sql.value_column.name, *sql.search_columns.map(&:name))
86
+ .map { |value, *attributes| result(value, attributes) }
87
+ .uniq
88
+ end
89
+
90
+ def result(value, attributes)
91
+ { value: value, attributes: attributes.reject { |attribute| attribute == value } }
92
+ end
93
+
94
+ def model
95
+ @model ||= params[:model].camelize(:upper).safe_constantize
96
+ end
97
+
98
+ def authorization
99
+ @authorization ||= TextSearch::Authorization.new(
100
+ model: model,
101
+ params: params,
102
+ user: controller.active_element.current_user,
103
+ search_columns: sql.search_columns.compact,
104
+ result_columns: (sql.search_columns + [sql.value_column]).compact
105
+ )
106
+ end
107
+
108
+ def query
109
+ params[:query]
110
+ end
111
+
112
+ def limit
113
+ DEFAULT_LIMIT
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ module TextSearch
6
+ # Encapsulates generation of database adapter-specific sanitized SQL queries for performing
7
+ # full text searches. Identifies columns and adapters where `LIKE` or `ILIKE` can be
8
+ # applied and generates a whereclause according to the provided parameters.
9
+ #
10
+ # Receives an ActiveRecord model class, a query (a string used to match results), attribute
11
+ # columns to match against, and a value column to return in the results.
12
+ #
13
+ # Inspects ActiveRecord metadata to match field names to column objects.
14
+ class Sql
15
+ def initialize(model:, query:, value:, attributes:)
16
+ @model = model
17
+ @query = query
18
+ @value = value
19
+ @attributes = attributes
20
+ end
21
+
22
+ def value_column
23
+ return nil if value.blank?
24
+
25
+ @value_column ||= model&.columns&.find { |column| column.name == value }
26
+ end
27
+
28
+ def search_columns
29
+ return [] if attributes.blank?
30
+
31
+ @search_columns ||= attributes.map { |attribute| column_for(attribute) }.compact
32
+ end
33
+
34
+ def whereclause
35
+ clauses = search_columns.map { |column| "#{column.name} #{operator(column)} ?" }
36
+ [clauses.join(' OR '), search_columns.map { |column| search_param(column) }].flatten
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :model, :query, :value, :attributes
42
+
43
+ def column_for(attribute)
44
+ matched_column = model&.columns&.find { |column| column.name == attribute }
45
+ return nil if matched_column.blank?
46
+
47
+ compatible_column?(matched_column) ? matched_column : nil
48
+ end
49
+
50
+ def operator(column)
51
+ case column.type
52
+ when :string
53
+ %w[Mysql2 SQLite].include?(model.connection.adapter_name) ? 'LIKE' : 'ILIKE'
54
+ else
55
+ '='
56
+ end
57
+ end
58
+
59
+ def compatible_column?(column) # rubocop:disable Metrics/MethodLength
60
+ case column.type
61
+ when :string
62
+ true
63
+ when :integer
64
+ integer?
65
+ when :float
66
+ float?
67
+ when :decimal
68
+ decimal?
69
+ else
70
+ Rails.logger.info("Skipping query `#{query}` for incompatible column: #{column.name}")
71
+ false
72
+ end
73
+ end
74
+
75
+ def integer?
76
+ Integer(query)
77
+ true
78
+ rescue ArgumentError
79
+ false
80
+ end
81
+
82
+ def float?
83
+ Float(query)
84
+ true
85
+ rescue ArgumentError
86
+ false
87
+ end
88
+
89
+ def decimal?
90
+ BigDecimal(query)
91
+ true
92
+ rescue ArgumentError
93
+ false
94
+ end
95
+
96
+ def search_param(column)
97
+ case column.type
98
+ when :string
99
+ "#{query}%"
100
+ else
101
+ query
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'text_search/active_record_authorization'
4
+ require_relative 'text_search/sql'
5
+ require_relative 'text_search/component'
6
+ require_relative 'text_search/authorization'
7
+
8
+ module ActiveElement
9
+ module Components
10
+ # Provides back end for live text search components.
11
+ module TextSearch
12
+ @authorized_text_searches = []
13
+
14
+ class << self
15
+ attr_reader :authorized_text_searches
16
+
17
+ def register_authorized_text_search(model:, with:, providing:)
18
+ authorized_text_searches << [model, with, providing]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -27,7 +27,7 @@ module ActiveElement
27
27
  component.controller.render_to_string(partial: decorator_path(sti: sti), locals: locals)
28
28
  rescue ActionView::MissingTemplate
29
29
  if sti
30
- component.controller.missing_template_store[decorator_path] = true
30
+ component.controller.active_element.missing_template_store[decorator_path] = true
31
31
  default_decorated_value
32
32
  else
33
33
  render(sti: true)
@@ -51,7 +51,7 @@ module ActiveElement
51
51
  end
52
52
 
53
53
  def missing_template?
54
- component.controller.missing_template_store[decorator_path].present?
54
+ component.controller.active_element.missing_template_store[decorator_path].present?
55
55
  end
56
56
 
57
57
  def decorator_path(sti: false)
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ module Util
6
+ # Generates Rails paths from records, used to generate form actions and links for Rails RESTful routes.
7
+ class RecordPath
8
+ def initialize(record:, controller:, type: nil)
9
+ @record = record
10
+ @controller = controller
11
+ @type = type&.to_sym || controller.action_name&.to_sym
12
+ end
13
+
14
+ def path
15
+ record_path || sti_record_path
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :record, :controller, :type
21
+
22
+ def namespace_prefix
23
+ # XXX: We guess the namespace from the current controller's module name. This will work
24
+ # most of the time but will break if the current record's controller exists in a different
25
+ # namespace to the current controller, e.g. `BackEndAdmin::UsersController` and
26
+ # `FrontEndAdmin::ThemesController` - if `FrontEndAdmin::ThemesController` renders a
27
+ # collection of `User` objects, the "show" path will be wrong:
28
+ # `front_end_admin_user_path`. Maybe descend through the full controller class tree to
29
+ # find a best match ?
30
+ namespace = controller.class.name.deconstantize.underscore
31
+ return nil if namespace.blank?
32
+
33
+ "#{namespace}_"
34
+ end
35
+
36
+ def record_path
37
+ return nil if record.nil?
38
+
39
+ controller.helpers.public_send(default_record_path, record)
40
+ rescue NoMethodError
41
+ controller.helpers.public_send(sti_record_path, record)
42
+ end
43
+
44
+ def default_record_path
45
+ "#{record_path_prefix}#{namespace_prefix}#{record_name}_path"
46
+ end
47
+
48
+ def sti_record_path
49
+ "#{record_path_prefix}#{namespace_prefix}#{sti_record_name}_path"
50
+ end
51
+
52
+ def record_name
53
+ return Util.record_name(record) unless pluralize?
54
+
55
+ Util.record_name(record)&.pluralize
56
+ end
57
+
58
+ def sti_record_name
59
+ return Util.sti_record_name(record) unless pluralize?
60
+
61
+ Util.sti_record_name(record)
62
+ end
63
+
64
+ def record_path_prefix
65
+ case type
66
+ when :edit, :update
67
+ 'edit_'
68
+ when :new, :create
69
+ 'new_'
70
+ end
71
+ end
72
+
73
+ def pluralize?
74
+ case type
75
+ when :index, :create
76
+ true
77
+ else
78
+ false
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'util/i18n'
4
+ require_relative 'util/record_path'
4
5
  require_relative 'util/record_mapping'
5
6
  require_relative 'util/field_mapping'
6
7
  require_relative 'util/form_field_mapping'
@@ -18,15 +19,19 @@ module ActiveElement
18
19
  end
19
20
 
20
21
  def self.record_name(record)
21
- record&.try(:model_name)&.try(&:singular) || record.class.name.demodulize.underscore
22
+ record&.try(:model_name)&.try(&:singular) || default_record_name(record)
22
23
  end
23
24
 
24
25
  def self.sti_record_name(record)
25
- return nil unless record.class.respond_to?(:inheritance_column)
26
+ return default_record_name(record) unless record.class.respond_to?(:inheritance_column)
26
27
 
27
28
  record&.class&.superclass&.model_name&.singular if record&.try(record.class.inheritance_column).present?
28
29
  end
29
30
 
31
+ def self.default_record_name(record)
32
+ record.class.name.demodulize.underscore
33
+ end
34
+
30
35
  def self.json_pretty_print(json)
31
36
  theme = Rouge::Themes::Base16.mode(:light)
32
37
  formatter = Rouge::Formatters::HTMLLinewise.new(Rouge::Formatters::HTMLInline.new(theme))
@@ -12,6 +12,7 @@ require_relative 'components/button'
12
12
  require_relative 'components/tabs'
13
13
  require_relative 'components/tab'
14
14
  require_relative 'components/json'
15
+ require_relative 'components/text_search'
15
16
 
16
17
  module ActiveElement
17
18
  # Standardised HTML components to be used in admin front ends.
@@ -3,15 +3,17 @@
3
3
  module ActiveElement
4
4
  # Processes all controller actions, verifies permissions, issues redirects, etc.
5
5
  class ControllerAction
6
+ include Paintbrush
7
+
6
8
  def initialize(controller)
7
9
  @controller = controller
8
10
  end
9
11
 
10
12
  def process_action
11
- Rails.logger.info("[#{log_tag}] #{colorized_permissions_message}")
13
+ Rails.logger.info("#{ActiveElement.log_tag} #{colorized_permissions_message}")
12
14
  return if verified_permissions?
13
15
 
14
- warn "[#{log_tag}] #{colorized_permissions_message}" if Rails.env.test?
16
+ warn "#{log_tag} #{colorized_permissions_message}" if Rails.env.test?
15
17
  return controller.redirect_to redirect_path if redirect_to_default_landing_page?
16
18
 
17
19
  render_forbidden
@@ -22,6 +24,7 @@ module ActiveElement
22
24
  attr_reader :controller
23
25
 
24
26
  def verified_permissions?
27
+ return true unless controller.active_element.authorize?
25
28
  return @verified_permissions if defined?(@verified_permissions)
26
29
 
27
30
  (@verified_permissions = permissions_check.permitted?)
@@ -50,8 +53,8 @@ module ActiveElement
50
53
 
51
54
  def permissions_check
52
55
  @permissions_check ||= PermissionsCheck.new(
53
- required: controller.class.active_element_permissions,
54
- actual: controller.current_user&.permissions,
56
+ required: controller.class.active_element.permissions,
57
+ actual: controller.active_element.current_user&.permissions,
55
58
  controller_path: controller.controller_path,
56
59
  action_name: controller.action_name,
57
60
  rails_component: rails_component
@@ -60,7 +63,7 @@ module ActiveElement
60
63
 
61
64
  def routes
62
65
  @routes ||= Routes.new(
63
- permissions: controller.current_user.permissions,
66
+ permissions: controller.active_element.current_user&.permissions,
64
67
  rails_component: rails_component
65
68
  )
66
69
  end
@@ -71,7 +74,7 @@ module ActiveElement
71
74
  else
72
75
  (rails_component.environment == 'test' ? :yellow : :red)
73
76
  end
74
- ColorizedString.new(permissions_check.message, color: color).value
77
+ paintbrush { public_send(color, permissions_check.message) }
75
78
  end
76
79
 
77
80
  def redirect_to_default_landing_page?
@@ -83,9 +86,5 @@ module ActiveElement
83
86
  def rails_component
84
87
  @rails_component ||= RailsComponent.new(::Rails)
85
88
  end
86
-
87
- def log_tag
88
- ColorizedString.new('ActiveElement', color: :cyan).value
89
- end
90
89
  end
91
90
  end