motor-admin 0.1.9 → 0.1.14

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 (52) 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/assets_controller.rb +15 -3
  5. data/app/controllers/motor/configs_controller.rb +2 -0
  6. data/app/controllers/motor/dashboards_controller.rb +2 -0
  7. data/app/controllers/motor/data_controller.rb +20 -1
  8. data/app/controllers/motor/forms_controller.rb +2 -0
  9. data/app/controllers/motor/queries_controller.rb +2 -0
  10. data/app/controllers/motor/resources_controller.rb +2 -0
  11. data/app/controllers/motor/run_queries_controller.rb +2 -0
  12. data/app/controllers/motor/send_alerts_controller.rb +2 -0
  13. data/app/models/motor/alert.rb +1 -1
  14. data/app/models/motor/alert_lock.rb +1 -1
  15. data/app/models/motor/config.rb +1 -1
  16. data/app/models/motor/dashboard.rb +1 -1
  17. data/app/models/motor/form.rb +1 -1
  18. data/app/models/motor/query.rb +1 -1
  19. data/app/models/motor/resource.rb +1 -1
  20. data/app/models/motor/tag.rb +1 -1
  21. data/app/models/motor/taggable_tag.rb +1 -1
  22. data/config/routes.rb +1 -0
  23. data/lib/motor.rb +1 -0
  24. data/lib/motor/active_record_utils.rb +2 -0
  25. data/lib/motor/active_record_utils/active_storage_links_extension.rb +15 -0
  26. data/lib/motor/active_record_utils/defined_scopes_extension.rb +19 -0
  27. data/lib/motor/admin.rb +14 -3
  28. data/lib/motor/alerts/persistance.rb +1 -2
  29. data/lib/motor/alerts/scheduled_alerts_cache.rb +0 -4
  30. data/lib/motor/api_query.rb +2 -0
  31. data/lib/motor/api_query/apply_scope.rb +26 -0
  32. data/lib/motor/api_query/build_json.rb +3 -2
  33. data/lib/motor/api_query/sort.rb +28 -8
  34. data/lib/motor/assets.rb +4 -4
  35. data/lib/motor/build_schema.rb +60 -7
  36. data/lib/motor/build_schema/active_storage_attachment_schema.rb +84 -0
  37. data/lib/motor/build_schema/find_display_column.rb +1 -1
  38. data/lib/motor/build_schema/load_from_rails.rb +112 -84
  39. data/lib/motor/build_schema/merge_schema_configs.rb +13 -6
  40. data/lib/motor/build_schema/persist_resource_configs.rb +38 -1
  41. data/lib/motor/build_schema/reorder_schema.rb +24 -2
  42. data/lib/motor/dashboards/persistance.rb +2 -2
  43. data/lib/motor/forms/persistance.rb +2 -2
  44. data/lib/motor/queries/persistance.rb +2 -2
  45. data/lib/motor/queries/run_query.rb +10 -5
  46. data/lib/motor/version.rb +1 -1
  47. data/ui/dist/main-f7c7f445c53544d2d7b8.css.gz +0 -0
  48. data/ui/dist/main-f7c7f445c53544d2d7b8.js.gz +0 -0
  49. data/ui/dist/manifest.json +5 -5
  50. metadata +9 -4
  51. data/ui/dist/main-46621a8bdbb789e17c3f.css.gz +0 -0
  52. data/ui/dist/main-46621a8bdbb789e17c3f.js.gz +0 -0
@@ -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
data/lib/motor/assets.rb CHANGED
@@ -18,16 +18,16 @@ module Motor
18
18
  Motor::Admin.routes.url_helpers.motor_asset_path(manifest[path])
19
19
  end
20
20
 
21
- def load_asset(filename)
21
+ def load_asset(filename, gzip: false)
22
22
  if Motor.development?
23
23
  load_from_dev_server(filename)
24
24
  else
25
- load_from_disk(filename)
25
+ load_from_disk(filename, gzip: gzip)
26
26
  end
27
27
  end
28
28
 
29
- def load_from_disk(filename)
30
- filename += '.gz' if filename.match?(/\.(?:js|css)\z/)
29
+ def load_from_disk(filename, gzip:)
30
+ filename += '.gz' if gzip
31
31
 
32
32
  path = ASSETS_PATH.join(filename)
33
33
 
@@ -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
@@ -31,7 +31,7 @@ module Motor
31
31
  address
32
32
  ].freeze
33
33
 
34
- DISPLAY_NAME_REGEXP = Regexp.new(Regexp.union(DISPLAY_NAMES), Regexp::IGNORECASE)
34
+ DISPLAY_NAME_REGEXP = Regexp.new(Regexp.union(DISPLAY_NAMES).source, Regexp::IGNORECASE)
35
35
 
36
36
  module_function
37
37
 
@@ -3,73 +3,28 @@
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
- end
11
+ rescue StandardError, NotImplementedError => e
12
+ Rails.logger.error(e)
13
+
14
+ next
15
+ end.compact
62
16
  end
63
17
 
64
18
  def models
65
- Rails.application.eager_load!
19
+ eager_load_models!
66
20
 
67
21
  models = load_descendants(ActiveRecord::Base).uniq
68
22
  models = models.reject(&:abstract_class)
69
23
 
70
24
  models -= Motor::ApplicationRecord.descendants
71
25
  models -= [ActiveRecord::SchemaMigration] if defined?(ActiveRecord::SchemaMigration)
72
- models -= [ActiveStorage::Blob, ActiveStorage::VariantRecord] if defined?(ActiveStorage::Blob)
26
+ models -= [ActiveStorage::Blob] if defined?(ActiveStorage::Blob)
27
+ models -= [ActiveStorage::VariantRecord] if defined?(ActiveStorage::VariantRecord)
73
28
 
74
29
  models
75
30
  end
@@ -81,40 +36,120 @@ module Motor
81
36
  end
82
37
 
83
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
+
84
43
  {
85
- name: model.name.underscore,
44
+ name: model_name.underscore,
86
45
  slug: Utils.slugify(model),
87
46
  table_name: model.table_name,
47
+ class_name: model.name,
88
48
  primary_key: model.primary_key,
89
- display_name: model.name.titleize.pluralize,
49
+ display_name: model_name.titleize.pluralize,
90
50
  display_column: FindDisplayColumn.call(model),
91
51
  columns: fetch_columns(model),
92
52
  associations: fetch_associations(model),
53
+ scopes: fetch_scopes(model),
93
54
  actions: DEFAULT_ACTIONS,
94
55
  tabs: DEFAULT_TABS,
95
56
  visible: true
96
57
  }.with_indifferent_access
97
58
  end
98
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
+
99
76
  def fetch_columns(model)
100
77
  default_attrs = model.new.attributes
101
78
 
102
- 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 build_column(model, column)
101
+ {
102
+ name: column.name,
103
+ display_name: column.name.humanize,
104
+ column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
105
+ access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
106
+ default_value: default_attrs[column.name],
107
+ validators: fetch_validators(model, column.name),
108
+ reference: fetch_reference(model, column.name),
109
+ virtual: false
110
+ }
111
+ end
112
+
113
+ def fetch_reference_columns(model)
114
+ default_attrs = model.new.attributes
115
+
116
+ model.reflections.map do |name, ref|
117
+ next if !ref.has_one? && !ref.belongs_to?
118
+
119
+ begin
120
+ ref.klass
121
+ rescue StandardError
122
+ next
123
+ end
124
+
125
+ column_name = ref.belongs_to? ? ref.foreign_key : name
126
+
127
+ next if ref.klass.name == 'ActiveStorage::Blob'
128
+
129
+ is_attachment = ref.klass.name == 'ActiveStorage::Attachment'
130
+
103
131
  {
104
- name: column.name,
105
- display_name: column.name.humanize,
106
- column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
107
- access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
108
- default_value: default_attrs[column.name],
109
- validators: fetch_validators(model, column.name),
132
+ name: column_name,
133
+ display_name: column_name.humanize,
134
+ column_type: is_attachment ? 'file' : 'integer',
135
+ access_type: ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY,
136
+ default_value: default_attrs[column_name],
137
+ validators: fetch_validators(model, column_name),
138
+ reference: {
139
+ name: name,
140
+ model_name: ref.klass.name.underscore,
141
+ reference_type: ref.belongs_to? ? 'belongs_to' : 'has_one',
142
+ foreign_key: ref.foreign_key,
143
+ polymorphic: ref.polymorphic? || is_attachment
144
+ },
110
145
  virtual: false
111
146
  }
112
- end
147
+ end.compact
113
148
  end
114
149
 
115
150
  def fetch_associations(model)
116
151
  model.reflections.map do |name, ref|
117
- next if ref.polymorphic? && !ref.belongs_to?
152
+ next if ref.has_one? || ref.belongs_to?
118
153
 
119
154
  begin
120
155
  ref.klass
@@ -122,37 +157,22 @@ module Motor
122
157
  next
123
158
  end
124
159
 
125
- next if defined?(ActiveStorage::Blob) && ref.klass == ActiveStorage::Blob
160
+ model_class = ref.klass
161
+
162
+ next if model_class.name == 'ActiveStorage::Blob'
126
163
 
127
164
  {
128
165
  name: name,
129
166
  display_name: name.humanize,
130
167
  slug: name.underscore,
131
- model_name: ref.klass.name.underscore,
132
- model_slug: Utils.slugify(ref.klass),
133
- association_type: fetch_association_type(ref),
168
+ model_name: model_class.name.underscore,
134
169
  foreign_key: ref.foreign_key,
135
- polymorphic: ref.polymorphic?,
170
+ polymorphic: ref.polymorphic? || model_class.name == 'ActiveStorage::Attachment',
136
171
  visible: true
137
172
  }
138
173
  end.compact
139
174
  end
140
175
 
141
- def fetch_association_type(association)
142
- case association.association_class.to_s
143
- when 'ActiveRecord::Associations::HasManyAssociation',
144
- 'ActiveRecord::Associations::HasManyThroughAssociation'
145
- 'has_many'
146
- when 'ActiveRecord::Associations::HasOneAssociation',
147
- 'ActiveRecord::Associations::HasOneThroughAssociation'
148
- 'has_one'
149
- when 'ActiveRecord::Associations::BelongsToAssociation'
150
- 'belongs_to'
151
- else
152
- raise ArgumentError, 'Unknown association type'
153
- end
154
- end
155
-
156
176
  def fetch_validators(model, column_name)
157
177
  model.validators_on(column_name).map do |validator|
158
178
  case validator
@@ -171,6 +191,14 @@ module Motor
171
191
  end
172
192
  end.compact
173
193
  end
194
+
195
+ def eager_load_models!
196
+ if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
197
+ Zeitwerk::Loader.eager_load_all
198
+ else
199
+ Rails.application.eager_load!
200
+ end
201
+ end
174
202
  end
175
203
  end
176
204
  end