motor-admin 0.1.10 → 0.1.16

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/motor/active_storage_attachments_controller.rb +28 -0
  3. data/app/controllers/motor/alerts_controller.rb +2 -0
  4. data/app/controllers/motor/configs_controller.rb +2 -0
  5. data/app/controllers/motor/dashboards_controller.rb +2 -0
  6. data/app/controllers/motor/data_controller.rb +20 -1
  7. data/app/controllers/motor/forms_controller.rb +2 -0
  8. data/app/controllers/motor/queries_controller.rb +2 -0
  9. data/app/controllers/motor/resources_controller.rb +2 -0
  10. data/app/controllers/motor/run_queries_controller.rb +2 -0
  11. data/app/controllers/motor/send_alerts_controller.rb +2 -0
  12. data/app/models/motor/alert.rb +1 -1
  13. data/app/models/motor/alert_lock.rb +1 -1
  14. data/app/models/motor/config.rb +1 -1
  15. data/app/models/motor/dashboard.rb +1 -1
  16. data/app/models/motor/form.rb +1 -1
  17. data/app/models/motor/query.rb +1 -1
  18. data/app/models/motor/resource.rb +1 -1
  19. data/app/models/motor/tag.rb +1 -1
  20. data/app/models/motor/taggable_tag.rb +1 -1
  21. data/config/routes.rb +1 -0
  22. data/lib/motor.rb +1 -0
  23. data/lib/motor/active_record_utils.rb +2 -0
  24. data/lib/motor/active_record_utils/active_storage_links_extension.rb +15 -0
  25. data/lib/motor/active_record_utils/defined_scopes_extension.rb +19 -0
  26. data/lib/motor/admin.rb +14 -3
  27. data/lib/motor/alerts/scheduled_alerts_cache.rb +0 -4
  28. data/lib/motor/api_query.rb +2 -0
  29. data/lib/motor/api_query/apply_scope.rb +26 -0
  30. data/lib/motor/api_query/build_json.rb +3 -2
  31. data/lib/motor/api_query/sort.rb +28 -8
  32. data/lib/motor/build_schema.rb +60 -7
  33. data/lib/motor/build_schema/active_storage_attachment_schema.rb +84 -0
  34. data/lib/motor/build_schema/load_from_rails.rb +93 -83
  35. data/lib/motor/build_schema/merge_schema_configs.rb +13 -6
  36. data/lib/motor/build_schema/persist_resource_configs.rb +38 -1
  37. data/lib/motor/build_schema/reorder_schema.rb +25 -3
  38. data/lib/motor/queries/run_query.rb +10 -5
  39. data/lib/motor/version.rb +1 -1
  40. data/ui/dist/main-729641083f64367ce9aa.css.gz +0 -0
  41. data/ui/dist/main-729641083f64367ce9aa.js.gz +0 -0
  42. data/ui/dist/manifest.json +5 -5
  43. metadata +9 -4
  44. data/ui/dist/main-4bdedd7fcff1351efaf6.css.gz +0 -0
  45. data/ui/dist/main-4bdedd7fcff1351efaf6.js.gz +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dffbef14f764d553575a908cee996e197c587b0db25dbe039c95094e0291e451
4
- data.tar.gz: 92ba80b363b93b28cd53a80cd0d9725f408f0a8269d0b7398080adfe2c55ad0c
3
+ metadata.gz: 4844a300bd3061e7c289d0c0e36f095922b32eafbb62c61d3e5f763dfe9508da
4
+ data.tar.gz: 95d56c11d15471a11c877ba319836c563d858f8c768b9c82a82fbb3d75f22b68
5
5
  SHA512:
6
- metadata.gz: fb50489810374fef3e44f5016660d4a73bb3e3234622b9452522e4050a48c50240ef296b24c7a1c78d73d202df648a60ae1fbcb3a957e20f1d4dac90b762c6ef
7
- data.tar.gz: e94dc6c92b8d96f6d974dffca9fe49698455d3c2229eada3fa43654caf60e671ff9b8a0c8183fc58ac8b1edeec52e68004d869d2fe6934af42a2b4d55e60943f
6
+ metadata.gz: ed58d6ab04a5fe3749ab22aaa281e4d4bc618ac652612bb005653a319da55d5ca6f0f4be6f36d445295033e57f6d8296a01750659cd830f0c8f4d95b5d0360cc
7
+ data.tar.gz: 5d13f12bce2d5547c3b429ff822c93ed053018a24f5c958da1dd931b08c9adec775c540765ed8e95593cfc9d8983578e8c3ca4943e8d6f48725767c0fb26cca5
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ActiveStorageAttachmentsController < ApiBaseController
5
+ wrap_parameters :data, except: %i[include fields]
6
+
7
+ load_and_authorize_resource :attachment, class: 'ActiveStorage::Attachment', parent: false
8
+
9
+ def create
10
+ if @attachment.record.respond_to?("#{@attachment.name}_attachment=") || @attachment.record.respond_to?("#{@attachment.name}_attachments=")
11
+ @attachment.record.public_send(@attachment.name).attach(
12
+ io: StringIO.new(params.dig(:data, :file, :io).to_s.encode('ISO-8859-1')),
13
+ filename: params.dig(:data, :file, :filename)
14
+ )
15
+
16
+ head :ok
17
+ else
18
+ head :unprocessable_entity
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def attachment_params
25
+ params.require(:data).except(:file).permit!
26
+ end
27
+ end
28
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Motor
4
4
  class AlertsController < ApiBaseController
5
+ wrap_parameters :data, except: %i[include fields]
6
+
5
7
  load_and_authorize_resource :alert, only: %i[index show update destroy]
6
8
 
7
9
  before_action :build_alert, only: :create
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Motor
4
4
  class ConfigsController < ApiBaseController
5
+ wrap_parameters :data, except: %i[include fields]
6
+
5
7
  load_and_authorize_resource
6
8
 
7
9
  def index
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Motor
4
4
  class DashboardsController < ApiBaseController
5
+ wrap_parameters :data, except: %i[include fields]
6
+
5
7
  load_and_authorize_resource :dashboard, only: %i[index show update destroy]
6
8
 
7
9
  before_action :build_dashboard, only: :create
@@ -4,8 +4,11 @@ module Motor
4
4
  class DataController < ApiBaseController
5
5
  INSTANCE_VARIABLE_NAME = 'resource'
6
6
 
7
+ wrap_parameters :data, except: %i[include fields]
8
+
7
9
  before_action :load_and_authorize_resource
8
10
  before_action :load_and_authorize_association
11
+ before_action :wrap_io_params
9
12
 
10
13
  def index
11
14
  @resources = Motor::ApiQuery.call(@resources, params)
@@ -96,7 +99,23 @@ module Motor
96
99
  end
97
100
 
98
101
  def resource_params
99
- params.fetch(:data, {}).except(resource_class.primary_key).permit!
102
+ if params[:data].present?
103
+ params.require(:data).except(resource_class.primary_key).permit!
104
+ else
105
+ {}
106
+ end
107
+ end
108
+
109
+ def wrap_io_params(hash = params)
110
+ hash.each do |key, value|
111
+ if key == 'io'
112
+ hash[key] = StringIO.new(value.encode('ISO-8859-1'))
113
+ elsif value.is_a?(ActionController::Parameters)
114
+ wrap_io_params(value)
115
+ end
116
+ end
117
+
118
+ hash
100
119
  end
101
120
  end
102
121
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Motor
4
4
  class FormsController < ApiBaseController
5
+ wrap_parameters :data, except: %i[include fields]
6
+
5
7
  load_and_authorize_resource :form, only: %i[index show update destroy]
6
8
 
7
9
  before_action :build_form, only: :create
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Motor
4
4
  class QueriesController < ApiBaseController
5
+ wrap_parameters :data, except: %i[include fields]
6
+
5
7
  load_and_authorize_resource :query, only: %i[index show update destroy]
6
8
 
7
9
  before_action :build_query, only: :create
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Motor
4
4
  class ResourcesController < ApiBaseController
5
+ wrap_parameters :data, except: %i[include fields]
6
+
5
7
  load_and_authorize_resource
6
8
 
7
9
  def index
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Motor
4
4
  class RunQueriesController < ApiBaseController
5
+ wrap_parameters :data
6
+
5
7
  load_and_authorize_resource :query, only: :show, parent: false
6
8
 
7
9
  before_action :build_query, only: :create
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Motor
4
4
  class SendAlertsController < ApiBaseController
5
+ wrap_parameters :data
6
+
5
7
  before_action :build_alert, only: :create
6
8
  authorize_resource :alert, only: :create
7
9
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- class Alert < ApplicationRecord
4
+ class Alert < ::Motor::ApplicationRecord
5
5
  belongs_to :query
6
6
  belongs_to :author, polymorphic: true, optional: true
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- class AlertLock < ApplicationRecord
4
+ class AlertLock < ::Motor::ApplicationRecord
5
5
  belongs_to :alert
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- class Config < ApplicationRecord
4
+ class Config < ::Motor::ApplicationRecord
5
5
  serialize :value, HashSerializer
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- class Dashboard < ApplicationRecord
4
+ class Dashboard < ::Motor::ApplicationRecord
5
5
  belongs_to :author, polymorphic: true, optional: true
6
6
 
7
7
  has_many :taggable_tags, as: :taggable
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- class Form < ApplicationRecord
4
+ class Form < ::Motor::ApplicationRecord
5
5
  belongs_to :author, polymorphic: true, optional: true
6
6
 
7
7
  has_many :taggable_tags, as: :taggable
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- class Query < ApplicationRecord
4
+ class Query < ::Motor::ApplicationRecord
5
5
  belongs_to :author, polymorphic: true, optional: true
6
6
 
7
7
  has_many :taggable_tags, as: :taggable
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- class Resource < ApplicationRecord
4
+ class Resource < ::Motor::ApplicationRecord
5
5
  serialize :preferences, HashSerializer
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- class Tag < ApplicationRecord
4
+ class Tag < ::Motor::ApplicationRecord
5
5
  has_many :taggable_tags
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- class TaggableTag < ApplicationRecord
4
+ class TaggableTag < ::Motor::ApplicationRecord
5
5
  belongs_to :tag
6
6
  belongs_to :taggable, polymorphic: true
7
7
  end
data/config/routes.rb CHANGED
@@ -13,6 +13,7 @@ Motor::Admin.routes.draw do
13
13
  resources :dashboards, only: %i[index show create update destroy]
14
14
  resources :forms, only: %i[index show create update destroy]
15
15
  resources :alerts, only: %i[index show create update destroy]
16
+ resources :active_storage_attachments, only: %i[create], path: 'data/active_storage__attachments'
16
17
  resource :schema, only: %i[show update]
17
18
  resources :resources, path: '/data/:resource',
18
19
  only: %i[index show update create destroy],
data/lib/motor.rb CHANGED
@@ -6,6 +6,7 @@ require 'js_regex'
6
6
  require 'fugit'
7
7
  require 'csv'
8
8
  require 'active_record/filter'
9
+ require 'base64'
9
10
 
10
11
  module Motor
11
12
  PATH = Pathname.new(__dir__)
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative './active_record_utils/types'
4
4
  require_relative './active_record_utils/fetch_methods'
5
+ require_relative './active_record_utils/defined_scopes_extension'
6
+ require_relative './active_record_utils/active_storage_links_extension'
5
7
 
6
8
  module ActiveRecordUtils
7
9
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ActiveRecordUtils
5
+ module ActiveStorageLinksExtension
6
+ def path
7
+ Rails.application.routes.url_helpers.rails_blob_path(self)
8
+ end
9
+
10
+ def url
11
+ Rails.application.routes.url_helpers.url_for(self)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ActiveRecordUtils
5
+ module DefinedScopesExtension
6
+ def scope(name, _body)
7
+ (@__scopes__ ||= []) << name
8
+
9
+ super
10
+ end
11
+
12
+ def defined_scopes
13
+ @__scopes__ || []
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ ActiveRecord::Base.extend(Motor::ActiveRecordUtils::DefinedScopesExtension)
data/lib/motor/admin.rb CHANGED
@@ -2,11 +2,22 @@
2
2
 
3
3
  module Motor
4
4
  class Admin < ::Rails::Engine
5
+ initializer 'motor.filter_params' do
6
+ Rails.application.config.filter_parameters += %i[io]
7
+ end
8
+
5
9
  initializer 'motor.alerts.scheduler' do
6
- next if defined?(Sidekiq) && Sidekiq.server?
10
+ config.after_initialize do |_app|
11
+ next unless defined?(Rails::Server)
12
+
13
+ Motor::Alerts::Scheduler::SCHEDULER_TASK.execute
14
+ end
15
+ end
7
16
 
8
- Motor::Alerts::Scheduler::SCHEDULER_TASK.execute
9
- Motor::Alerts::ScheduledAlertsCache::UPDATE_ALERTS_TASK.execute
17
+ initializer 'motor.active_storage.extensions' do
18
+ ActiveSupport.on_load(:active_storage_attachment) do
19
+ ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
20
+ end
10
21
  end
11
22
  end
12
23
  end
@@ -3,10 +3,6 @@
3
3
  module Motor
4
4
  module Alerts
5
5
  module ScheduledAlertsCache
6
- UPDATE_ALERTS_TASK = Concurrent::TimerTask.new(
7
- execution_interval: 2.minutes
8
- ) { Motor::Alerts::ScheduledAlertsCache.load_alerts }
9
-
10
6
  CACHE_STORE = ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
11
7
 
12
8
  module_function
@@ -4,6 +4,7 @@ require_relative './api_query/sort'
4
4
  require_relative './api_query/paginate'
5
5
  require_relative './api_query/filter'
6
6
  require_relative './api_query/search'
7
+ require_relative './api_query/apply_scope'
7
8
  require_relative './api_query/build_meta'
8
9
  require_relative './api_query/build_json'
9
10
 
@@ -15,6 +16,7 @@ module Motor
15
16
  rel = ApiQuery::Sort.call(rel, params[:sort])
16
17
  rel = ApiQuery::Paginate.call(rel, params[:page])
17
18
  rel = ApiQuery::Filter.call(rel, params[:filter])
19
+ rel = ApiQuery::ApplyScope.call(rel, params[:scope])
18
20
 
19
21
  ApiQuery::Search.call(rel, params[:q] || params[:search] || params[:query])
20
22
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module ApplyScope
6
+ module_function
7
+
8
+ def call(rel, scope)
9
+ return rel if scope.blank?
10
+
11
+ scope_symbol = scope.to_sym
12
+
13
+ if rel.klass.defined_scopes.include?(scope_symbol)
14
+ rel.public_send(scope_symbol)
15
+ else
16
+ configs = Motor::Resource.find_by_name(rel.klass.name.underscore)
17
+ scope_configs = configs.preferences[:scopes].find { |s| s[:name] == scope }
18
+
19
+ return rel unless scope_configs
20
+
21
+ ApiQuery::Filter.call(rel, scope_configs[:preferences][:filter])
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -49,7 +49,8 @@ module Motor
49
49
 
50
50
  params[:fields].each do |key, fields|
51
51
  fields = fields.split(',') if fields.is_a?(String)
52
- fields_hash = build_fields_hash(model, fields)
52
+ reflection_class = model.reflections[key]&.klass
53
+ fields_hash = build_fields_hash(reflection_class || model, fields)
53
54
 
54
55
  if key == model_name || model_name.split('/').last == key
55
56
  json_params.merge!(fields_hash)
@@ -68,7 +69,7 @@ module Motor
68
69
  fields.each_with_object(fields_hash) do |field, acc|
69
70
  if field.in?(columns)
70
71
  acc['only'] << field
71
- else
72
+ elsif model.instance_methods.include?(field.to_sym)
72
73
  acc['methods'] << field
73
74
  end
74
75
  end
@@ -7,19 +7,39 @@ module Motor
7
7
 
8
8
  module_function
9
9
 
10
- def call(rel, params)
11
- return rel if params.blank?
10
+ def call(rel, param)
11
+ return rel if param.blank?
12
12
 
13
- normalized_params = build_params(params)
13
+ arel_order = build_arel_order(rel.klass, param)
14
+ join_params = build_join_params(rel.klass, param)
14
15
 
15
- rel.order(normalized_params)
16
+ rel.order(arel_order).left_joins(join_params)
16
17
  end
17
18
 
18
- def build_params(param)
19
- param.split(',').each_with_object({}) do |field, hash|
20
- direction, name = field.match(FIELD_PARSE_REGEXP).captures
19
+ def build_join_params(_model, param)
20
+ param.split(',').each_with_object({}) do |field, result|
21
+ key = field[FIELD_PARSE_REGEXP, 2]
22
+ *path, _ = key.split('.')
21
23
 
22
- hash[name] = direction.present? ? :desc : :asc
24
+ path.reduce(result) do |acc, fragment|
25
+ acc[fragment] = {}
26
+ end
27
+ end
28
+ end
29
+
30
+ def build_arel_order(model, param)
31
+ param.split(',').map do |field|
32
+ direction, key = field.match(FIELD_PARSE_REGEXP).captures
33
+ *path, field = key.split('.')
34
+
35
+ reflection_model =
36
+ path.reduce(model) do |acc, fragment|
37
+ acc.reflections[fragment].klass
38
+ end
39
+
40
+ arel_column = reflection_model.arel_table[field]
41
+
42
+ direction.present? ? arel_column.desc : arel_column.asc
23
43
  end
24
44
  end
25
45
  end
@@ -1,16 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './build_schema/load_from_rails'
4
- require_relative './build_schema/find_display_column'
5
- require_relative './build_schema/persist_resource_configs'
6
- require_relative './build_schema/reorder_schema'
7
- require_relative './build_schema/merge_schema_configs'
8
- require_relative './build_schema/utils'
9
-
10
3
  module Motor
11
4
  module BuildSchema
5
+ module ColumnAccessTypes
6
+ ALL = [
7
+ READ_ONLY = 'read_only',
8
+ WRITE_ONLY = 'write_only',
9
+ READ_WRITE = 'read_write',
10
+ HIDDEN = 'hidden'
11
+ ].freeze
12
+ end
13
+
12
14
  SEARCHABLE_COLUMN_TYPES = %i[citext text string bitstring].freeze
13
15
 
16
+ COLUMN_NAME_ACCESS_TYPES = {
17
+ id: ColumnAccessTypes::READ_ONLY,
18
+ created_at: ColumnAccessTypes::READ_ONLY,
19
+ updated_at: ColumnAccessTypes::READ_ONLY,
20
+ deleted_at: ColumnAccessTypes::READ_ONLY
21
+ }.with_indifferent_access.freeze
22
+
23
+ DEFAULT_SCOPE_TYPE = 'default'
24
+
25
+ DEFAULT_ACTIONS = [
26
+ {
27
+ name: 'create',
28
+ display_name: 'Create',
29
+ action_type: 'default',
30
+ preferences: {},
31
+ visible: true
32
+ },
33
+ {
34
+ name: 'edit',
35
+ display_name: 'Edit',
36
+ action_type: 'default',
37
+ preferences: {},
38
+ visible: true
39
+ },
40
+ {
41
+ name: 'remove',
42
+ display_name: 'Remove',
43
+ action_type: 'default',
44
+ preferences: {},
45
+ visible: true
46
+ }
47
+ ].freeze
48
+
49
+ DEFAULT_TABS = [
50
+ {
51
+ name: 'summary',
52
+ display_name: 'Summary',
53
+ tab_type: 'default',
54
+ preferences: {},
55
+ visible: true
56
+ }
57
+ ].freeze
58
+
14
59
  module_function
15
60
 
16
61
  def call
@@ -21,3 +66,11 @@ module Motor
21
66
  end
22
67
  end
23
68
  end
69
+
70
+ require_relative './build_schema/active_storage_attachment_schema'
71
+ require_relative './build_schema/load_from_rails'
72
+ require_relative './build_schema/find_display_column'
73
+ require_relative './build_schema/persist_resource_configs'
74
+ require_relative './build_schema/reorder_schema'
75
+ require_relative './build_schema/merge_schema_configs'
76
+ require_relative './build_schema/utils'
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ ACTIVE_STORAGE_ATTACHMENT_SCHEMA = {
6
+ name: 'active_storage/attachment',
7
+ slug: 'active_storage__attachments',
8
+ table_name: 'active_storage_attachments',
9
+ primary_key: 'id',
10
+ display_name: 'Attachments',
11
+ display_column: 'filename',
12
+ columns: [
13
+ {
14
+ name: 'id',
15
+ display_name: 'Id',
16
+ column_type: 'integer',
17
+ access_type: 'read_only',
18
+ default_value: nil,
19
+ validators: [],
20
+ virtual: false
21
+ },
22
+ {
23
+ name: 'path',
24
+ display_name: 'Path',
25
+ column_type: 'string',
26
+ access_type: 'read_only',
27
+ default_value: nil,
28
+ validators: [],
29
+ virtual: true
30
+ },
31
+ {
32
+ name: 'name',
33
+ display_name: 'Name',
34
+ column_type: 'string',
35
+ access_type: 'read_write',
36
+ default_value: nil,
37
+ validators: [],
38
+ virtual: false
39
+ },
40
+ {
41
+ name: 'record_type',
42
+ display_name: 'Record type',
43
+ column_type: 'string',
44
+ access_type: 'read_write',
45
+ default_value: nil,
46
+ validators: [],
47
+ virtual: false
48
+ },
49
+ {
50
+ name: 'record_id',
51
+ display_name: 'Record',
52
+ column_type: 'integer',
53
+ access_type: 'read_write',
54
+ default_value: nil,
55
+ validators: [],
56
+ virtual: false
57
+ },
58
+ {
59
+ name: 'file',
60
+ display_name: 'File',
61
+ column_type: 'file',
62
+ access_type: 'write_only',
63
+ default_value: nil,
64
+ validators: [],
65
+ virtual: false
66
+ },
67
+ {
68
+ name: 'created_at',
69
+ display_name: 'Created at',
70
+ column_type: 'datetime',
71
+ access_type: 'read_only',
72
+ default_value: nil,
73
+ validators: [],
74
+ virtual: false
75
+ }
76
+ ],
77
+ associations: [],
78
+ scopes: [],
79
+ actions: Motor::BuildSchema::DEFAULT_ACTIONS.reject { |e| e[:name] == 'edit' },
80
+ tabs: Motor::BuildSchema::DEFAULT_TABS,
81
+ visible: true
82
+ }.with_indifferent_access
83
+ end
84
+ end
@@ -3,62 +3,12 @@
3
3
  module Motor
4
4
  module BuildSchema
5
5
  module LoadFromRails
6
- module ColumnAccessTypes
7
- ALL = [
8
- READ_ONLY = 'read_only',
9
- WRITE_ONLY = 'write_only',
10
- READ_WRITE = 'read_write',
11
- HIDDEN = 'hidden'
12
- ].freeze
13
- end
14
-
15
- COLUMN_NAME_ACCESS_TYPES = {
16
- id: ColumnAccessTypes::READ_ONLY,
17
- created_at: ColumnAccessTypes::READ_ONLY,
18
- updated_at: ColumnAccessTypes::READ_ONLY,
19
- deleted_at: ColumnAccessTypes::READ_ONLY
20
- }.with_indifferent_access.freeze
21
-
22
- DEFAULT_ACTIONS = [
23
- {
24
- name: 'create',
25
- display_name: 'Create',
26
- action_type: 'default',
27
- preferences: {},
28
- visible: true
29
- },
30
- {
31
- name: 'edit',
32
- display_name: 'Edit',
33
- action_type: 'default',
34
- preferences: {},
35
- visible: true
36
- },
37
- {
38
- name: 'remove',
39
- display_name: 'Remove',
40
- action_type: 'default',
41
- preferences: {},
42
- visible: true
43
- }
44
- ].freeze
45
-
46
- DEFAULT_TABS = [
47
- {
48
- name: 'summary',
49
- display_name: 'Summary',
50
- tab_type: 'default',
51
- preferences: {},
52
- visible: true
53
- }
54
- ].freeze
55
-
56
6
  module_function
57
7
 
58
8
  def call
59
9
  models.map do |model|
60
10
  build_model_schema(model)
61
- rescue StandardError => e
11
+ rescue StandardError, NotImplementedError => e
62
12
  Rails.logger.error(e)
63
13
 
64
14
  next
@@ -66,7 +16,7 @@ module Motor
66
16
  end
67
17
 
68
18
  def models
69
- Rails.application.eager_load!
19
+ eager_load_models!
70
20
 
71
21
  models = load_descendants(ActiveRecord::Base).uniq
72
22
  models = models.reject(&:abstract_class)
@@ -86,40 +36,107 @@ module Motor
86
36
  end
87
37
 
88
38
  def build_model_schema(model)
39
+ model_name = model.name
40
+
41
+ return Motor::BuildSchema::ACTIVE_STORAGE_ATTACHMENT_SCHEMA if model_name == 'ActiveStorage::Attachment'
42
+
89
43
  {
90
- name: model.name.underscore,
44
+ name: model_name.underscore,
91
45
  slug: Utils.slugify(model),
92
46
  table_name: model.table_name,
47
+ class_name: model.name,
93
48
  primary_key: model.primary_key,
94
- display_name: model.name.titleize.pluralize,
49
+ display_name: model_name.titleize.pluralize,
95
50
  display_column: FindDisplayColumn.call(model),
96
51
  columns: fetch_columns(model),
97
52
  associations: fetch_associations(model),
53
+ scopes: fetch_scopes(model),
98
54
  actions: DEFAULT_ACTIONS,
99
55
  tabs: DEFAULT_TABS,
100
56
  visible: true
101
57
  }.with_indifferent_access
102
58
  end
103
59
 
60
+ def fetch_scopes(model)
61
+ model.defined_scopes.map do |scope_name|
62
+ scope_name = scope_name.to_s
63
+
64
+ next if scope_name.starts_with?('with_attached')
65
+
66
+ {
67
+ name: scope_name,
68
+ display_name: scope_name.humanize,
69
+ scope_type: DEFAULT_SCOPE_TYPE,
70
+ visible: true,
71
+ preferences: {}
72
+ }
73
+ end.compact
74
+ end
75
+
104
76
  def fetch_columns(model)
105
77
  default_attrs = model.new.attributes
106
78
 
107
- model.columns.map do |column|
79
+ reference_columns = fetch_reference_columns(model)
80
+
81
+ table_columns =
82
+ model.columns.map do |column|
83
+ next if reference_columns.find { |c| c[:name] == column.name }
84
+
85
+ {
86
+ name: column.name,
87
+ display_name: column.name.humanize,
88
+ column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
89
+ access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
90
+ default_value: default_attrs[column.name],
91
+ validators: fetch_validators(model, column.name),
92
+ reference: nil,
93
+ virtual: false
94
+ }
95
+ end.compact
96
+
97
+ reference_columns + table_columns
98
+ end
99
+
100
+ def fetch_reference_columns(model)
101
+ default_attrs = model.new.attributes
102
+
103
+ model.reflections.map do |name, ref|
104
+ next if !ref.has_one? && !ref.belongs_to?
105
+
106
+ begin
107
+ ref.klass
108
+ rescue StandardError
109
+ next
110
+ end
111
+
112
+ column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
113
+
114
+ next if ref.klass.name == 'ActiveStorage::Blob'
115
+
116
+ is_attachment = ref.klass.name == 'ActiveStorage::Attachment'
117
+
108
118
  {
109
- name: column.name,
110
- display_name: column.name.humanize,
111
- column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
112
- access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
113
- default_value: default_attrs[column.name],
114
- validators: fetch_validators(model, column.name),
119
+ name: column_name,
120
+ display_name: column_name.humanize,
121
+ column_type: is_attachment ? 'file' : 'integer',
122
+ access_type: ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY,
123
+ default_value: default_attrs[column_name],
124
+ validators: fetch_validators(model, column_name),
125
+ reference: {
126
+ name: name,
127
+ model_name: ref.klass.name.underscore,
128
+ reference_type: ref.belongs_to? ? 'belongs_to' : 'has_one',
129
+ foreign_key: ref.foreign_key,
130
+ polymorphic: ref.polymorphic? || is_attachment
131
+ },
115
132
  virtual: false
116
133
  }
117
- end
134
+ end.compact
118
135
  end
119
136
 
120
137
  def fetch_associations(model)
121
138
  model.reflections.map do |name, ref|
122
- next if ref.polymorphic? && !ref.belongs_to?
139
+ next if ref.has_one? || ref.belongs_to?
123
140
 
124
141
  begin
125
142
  ref.klass
@@ -127,37 +144,22 @@ module Motor
127
144
  next
128
145
  end
129
146
 
130
- next if defined?(ActiveStorage::Blob) && ref.klass == ActiveStorage::Blob
147
+ model_class = ref.klass
148
+
149
+ next if model_class.name == 'ActiveStorage::Blob'
131
150
 
132
151
  {
133
152
  name: name,
134
153
  display_name: name.humanize,
135
154
  slug: name.underscore,
136
- model_name: ref.klass.name.underscore,
137
- model_slug: Utils.slugify(ref.klass),
138
- association_type: fetch_association_type(ref),
155
+ model_name: model_class.name.underscore,
139
156
  foreign_key: ref.foreign_key,
140
- polymorphic: ref.polymorphic?,
157
+ polymorphic: ref.polymorphic? || model_class.name == 'ActiveStorage::Attachment',
141
158
  visible: true
142
159
  }
143
160
  end.compact
144
161
  end
145
162
 
146
- def fetch_association_type(association)
147
- case association.association_class.to_s
148
- when 'ActiveRecord::Associations::HasManyAssociation',
149
- 'ActiveRecord::Associations::HasManyThroughAssociation'
150
- 'has_many'
151
- when 'ActiveRecord::Associations::HasOneAssociation',
152
- 'ActiveRecord::Associations::HasOneThroughAssociation'
153
- 'has_one'
154
- when 'ActiveRecord::Associations::BelongsToAssociation'
155
- 'belongs_to'
156
- else
157
- raise ArgumentError, 'Unknown association type'
158
- end
159
- end
160
-
161
163
  def fetch_validators(model, column_name)
162
164
  model.validators_on(column_name).map do |validator|
163
165
  case validator
@@ -176,6 +178,14 @@ module Motor
176
178
  end
177
179
  end.compact
178
180
  end
181
+
182
+ def eager_load_models!
183
+ if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
184
+ Zeitwerk::Loader.eager_load_all
185
+ else
186
+ Rails.application.eager_load!
187
+ end
188
+ end
179
189
  end
180
190
  end
181
191
  end
@@ -7,6 +7,7 @@ module Motor
7
7
  COLUMN_DEFAULTS = PersistResourceConfigs::COLUMN_DEFAULTS
8
8
  ACTION_DEFAULTS = PersistResourceConfigs::ACTION_DEFAULTS
9
9
  TAB_DEFAULTS = PersistResourceConfigs::TAB_DEFAULTS
10
+ SCOPE_DEFAULTS = PersistResourceConfigs::SCOPE_DEFAULTS
10
11
 
11
12
  module_function
12
13
 
@@ -26,17 +27,17 @@ module Motor
26
27
  def merge_model(model, configs)
27
28
  updated_model = model.merge(configs.slice(*RESOURCE_ATTRS))
28
29
 
30
+ updated_model[:associations] = merge_by_name(
31
+ model[:associations],
32
+ configs[:associations]
33
+ )
34
+
29
35
  updated_model[:columns] = merge_by_name(
30
36
  model[:columns],
31
37
  configs[:columns],
32
38
  COLUMN_DEFAULTS
33
39
  )
34
40
 
35
- updated_model[:associations] = merge_by_name(
36
- model[:associations],
37
- configs[:associations]
38
- )
39
-
40
41
  updated_model[:actions] = merge_by_name(
41
42
  model[:actions],
42
43
  configs[:actions],
@@ -46,7 +47,13 @@ module Motor
46
47
  updated_model[:tabs] = merge_by_name(
47
48
  model[:tabs],
48
49
  configs[:tabs],
49
- ACTION_DEFAULTS
50
+ TAB_DEFAULTS
51
+ )
52
+
53
+ updated_model[:scopes] = merge_by_name(
54
+ model[:scopes],
55
+ configs[:scopes],
56
+ SCOPE_DEFAULTS
50
57
  )
51
58
 
52
59
  updated_model
@@ -6,12 +6,14 @@ module Motor
6
6
  RESOURCE_ATTRS = %w[display_name visible].freeze
7
7
  COLUMN_ATTRS = %w[name display_name column_type access_type default_value virtual].freeze
8
8
  ASSOCIATION_ATTRS = %w[name display_name visible].freeze
9
+ SCOPE_ATTRS = %w[name display_name scope_type preferences visible].freeze
9
10
  ACTION_ATTRS = %w[name display_name action_type preferences visible].freeze
10
11
  TAB_ATTRS = %w[name display_name tab_type preferences visible].freeze
11
12
 
12
13
  COLUMN_DEFAULTS = {
13
14
  access_type: 'read_write',
14
15
  default_value: nil,
16
+ reference: nil,
15
17
  validators: []
16
18
  }.with_indifferent_access
17
19
 
@@ -25,6 +27,12 @@ module Motor
25
27
  preferences: {}
26
28
  }.with_indifferent_access
27
29
 
30
+ SCOPE_DEFAULTS = {
31
+ visible: true,
32
+ scope_type: 'default',
33
+ preferences: {}
34
+ }.with_indifferent_access
35
+
28
36
  module_function
29
37
 
30
38
  # @param resource [Motor::Resource]
@@ -99,6 +107,14 @@ module Motor
99
107
  )
100
108
  end
101
109
 
110
+ if new_prefs[:scopes].present?
111
+ normalized_preferences[:scopes] = normalize_scopes(
112
+ default_prefs[:scopes],
113
+ existing_prefs.fetch(:scopes, []),
114
+ new_prefs.fetch(:scopes, [])
115
+ )
116
+ end
117
+
102
118
  normalized_preferences.compact
103
119
  end
104
120
 
@@ -138,7 +154,7 @@ module Motor
138
154
  action_attrs = new_action.slice(*ACTION_ATTRS)
139
155
 
140
156
  normalized_action = existing_action.merge(action_attrs)
141
- normalized_action = reject_default(default_action.presence || TAB_DEFAULTS, normalized_action)
157
+ normalized_action = reject_default(default_action.presence || ACTION_DEFAULTS, normalized_action)
142
158
 
143
159
  normalized_action.merge(name: name) if normalized_action.present?
144
160
  end.compact.presence
@@ -165,6 +181,27 @@ module Motor
165
181
  end.compact.presence
166
182
  end
167
183
 
184
+ # @param default_scopes [Array<HashWithIndifferentAccess>]
185
+ # @param existing_scopes [Array<HashWithIndifferentAccess>]
186
+ # @param new_scopes [Array<HashWithIndifferentAccess>]
187
+ # @return [Array<HashWithIndifferentAccess>]
188
+ def normalize_scopes(default_scopes, existing_scopes, new_scopes)
189
+ (existing_scopes.pluck(:name) + new_scopes.pluck(:name)).uniq.map do |name|
190
+ new_scope = safe_fetch_by_name(new_scopes, name)
191
+
192
+ next if new_scope[:_remove]
193
+
194
+ existing_scope = safe_fetch_by_name(existing_scopes, name)
195
+ default_scope = safe_fetch_by_name(default_scopes, name)
196
+ scope_attrs = new_scope.slice(*SCOPE_ATTRS)
197
+
198
+ normalized_scope = existing_scope.merge(scope_attrs)
199
+ normalized_scope = reject_default(default_scope.presence || SCOPE_DEFAULTS, normalized_scope)
200
+
201
+ normalized_scope.merge(name: name) if normalized_scope.present?
202
+ end.compact.presence
203
+ end
204
+
168
205
  # @param default_assocs [Array<HashWithIndifferentAccess>]
169
206
  # @param existing_assocs [Array<HashWithIndifferentAccess>]
170
207
  # @param new_assocs [Array<HashWithIndifferentAccess>]
@@ -2,6 +2,18 @@
2
2
 
3
3
  module Motor
4
4
  module ReorderSchema
5
+ COLUMNS_DEFAULT_ORDER_WEIGHTS = {
6
+ id: 0,
7
+ updated_at: 2,
8
+ edited_at: 2,
9
+ created_at: 3,
10
+ inserted_at: 3,
11
+ deleted_at: 4,
12
+ archived_at: 4
13
+ }.with_indifferent_access
14
+
15
+ COLUMNS_DEFAULT_ORDER_WEIGHT = 1
16
+
5
17
  module_function
6
18
 
7
19
  # @param schema [Array<HashWithIndifferentAccess>]
@@ -16,12 +28,14 @@ module Motor
16
28
  associations_order = configs["resources.#{model[:name]}.associations.order"]
17
29
  actions_order = configs["resources.#{model[:name]}.actions.order"]
18
30
  tabs_order = configs["resources.#{model[:name]}.tabs.order"]
31
+ scopes_order = configs["resources.#{model[:name]}.scopes.order"]
19
32
 
20
33
  model.merge(
21
- columns: sort_by_name(model[:columns], columns_order, sort_alphabetically: false),
34
+ columns: sort_by_name(sort_columns(model[:columns]), columns_order, sort_alphabetically: false),
22
35
  associations: sort_by_name(model[:associations], associations_order),
23
36
  actions: sort_by_name(model[:actions], actions_order, sort_alphabetically: false),
24
- tabs: sort_by_name(model[:tabs], tabs_order, sort_alphabetically: false)
37
+ tabs: sort_by_name(model[:tabs], tabs_order, sort_alphabetically: false),
38
+ scopes: sort_by_name(model[:scopes], scopes_order)
25
39
  )
26
40
  end
27
41
  end
@@ -37,11 +51,19 @@ module Motor
37
51
  if order.present?
38
52
  order.index(item[:name]) || Float::MAX
39
53
  else
40
- item[:display_name]
54
+ item[:display_name].to_s
41
55
  end
42
56
  end
43
57
  end
44
58
 
59
+ def sort_columns(columns)
60
+ columns.each_with_object([]) do |column, acc|
61
+ weight = COLUMNS_DEFAULT_ORDER_WEIGHTS.fetch(column[:name], COLUMNS_DEFAULT_ORDER_WEIGHT)
62
+
63
+ (acc[weight] ||= []) << column
64
+ end.flatten.compact
65
+ end
66
+
45
67
  # @return [Hash<String, HashWithIndifferentAccess>]
46
68
  def load_configs
47
69
  Motor::Config.all.each_with_object({}) do |config, acc|
@@ -57,11 +57,16 @@ module Motor
57
57
  "$#{index}"
58
58
  end
59
59
 
60
- [
61
- format(WITH_STATEMENT_TEMPLATE, sql_body: sql.strip.gsub(/;\z/, ''), limit: limit),
62
- 'SQL',
63
- variables.map { |variable_name, default_value| variables_hash[variable_name] || default_value }
64
- ]
60
+ attributes =
61
+ variables.map do |variable_name, default_value|
62
+ ActiveRecord::Relation::QueryAttribute.new(
63
+ variable_name,
64
+ variables_hash[variable_name] || default_value,
65
+ ActiveRecord::Type::Value.new
66
+ )
67
+ end
68
+
69
+ [format(WITH_STATEMENT_TEMPLATE, sql_body: sql.strip.gsub(/;\z/, ''), limit: limit), 'SQL', attributes]
65
70
  end
66
71
  end
67
72
  end
data/lib/motor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motor
4
- VERSION = '0.1.10'
4
+ VERSION = '0.1.16'
5
5
  end
@@ -5,9 +5,9 @@
5
5
  "fonts/ionicons.ttf?v=3.0.0-alpha.3": "fonts/ionicons.ttf",
6
6
  "fonts/ionicons.woff2?v=3.0.0-alpha.3": "fonts/ionicons.woff2",
7
7
  "fonts/ionicons.woff?v=3.0.0-alpha.3": "fonts/ionicons.woff",
8
- "main-4bdedd7fcff1351efaf6.css.gz": "main-4bdedd7fcff1351efaf6.css.gz",
9
- "main-4bdedd7fcff1351efaf6.js.LICENSE.txt": "main-4bdedd7fcff1351efaf6.js.LICENSE.txt",
10
- "main-4bdedd7fcff1351efaf6.js.gz": "main-4bdedd7fcff1351efaf6.js.gz",
11
- "main.css": "main-4bdedd7fcff1351efaf6.css",
12
- "main.js": "main-4bdedd7fcff1351efaf6.js"
8
+ "main-729641083f64367ce9aa.css.gz": "main-729641083f64367ce9aa.css.gz",
9
+ "main-729641083f64367ce9aa.js.LICENSE.txt": "main-729641083f64367ce9aa.js.LICENSE.txt",
10
+ "main-729641083f64367ce9aa.js.gz": "main-729641083f64367ce9aa.js.gz",
11
+ "main.css": "main-729641083f64367ce9aa.css",
12
+ "main.js": "main-729641083f64367ce9aa.js"
13
13
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: motor-admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pete Matsyburka
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-26 00:00:00.000000000 Z
11
+ date: 2021-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord-filter
@@ -132,6 +132,7 @@ files:
132
132
  - LICENSE
133
133
  - README.md
134
134
  - Rakefile
135
+ - app/controllers/motor/active_storage_attachments_controller.rb
135
136
  - app/controllers/motor/alerts_controller.rb
136
137
  - app/controllers/motor/api_base_controller.rb
137
138
  - app/controllers/motor/application_controller.rb
@@ -172,6 +173,8 @@ files:
172
173
  - lib/motor-admin.rb
173
174
  - lib/motor.rb
174
175
  - lib/motor/active_record_utils.rb
176
+ - lib/motor/active_record_utils/active_storage_links_extension.rb
177
+ - lib/motor/active_record_utils/defined_scopes_extension.rb
175
178
  - lib/motor/active_record_utils/fetch_methods.rb
176
179
  - lib/motor/active_record_utils/types.rb
177
180
  - lib/motor/admin.rb
@@ -181,6 +184,7 @@ files:
181
184
  - lib/motor/alerts/scheduler.rb
182
185
  - lib/motor/api.rb
183
186
  - lib/motor/api_query.rb
187
+ - lib/motor/api_query/apply_scope.rb
184
188
  - lib/motor/api_query/build_json.rb
185
189
  - lib/motor/api_query/build_meta.rb
186
190
  - lib/motor/api_query/filter.rb
@@ -189,6 +193,7 @@ files:
189
193
  - lib/motor/api_query/sort.rb
190
194
  - lib/motor/assets.rb
191
195
  - lib/motor/build_schema.rb
196
+ - lib/motor/build_schema/active_storage_attachment_schema.rb
192
197
  - lib/motor/build_schema/find_display_column.rb
193
198
  - lib/motor/build_schema/load_from_rails.rb
194
199
  - lib/motor/build_schema/merge_schema_configs.rb
@@ -208,8 +213,8 @@ files:
208
213
  - lib/motor/ui_configs.rb
209
214
  - lib/motor/version.rb
210
215
  - ui/dist/fonts/ionicons.woff2
211
- - ui/dist/main-4bdedd7fcff1351efaf6.css.gz
212
- - ui/dist/main-4bdedd7fcff1351efaf6.js.gz
216
+ - ui/dist/main-729641083f64367ce9aa.css.gz
217
+ - ui/dist/main-729641083f64367ce9aa.js.gz
213
218
  - ui/dist/manifest.json
214
219
  homepage:
215
220
  licenses: