motor-admin 0.1.10 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/motor/active_storage_attachments_controller.rb +28 -0
- data/app/controllers/motor/alerts_controller.rb +2 -0
- data/app/controllers/motor/configs_controller.rb +2 -0
- data/app/controllers/motor/dashboards_controller.rb +2 -0
- data/app/controllers/motor/data_controller.rb +20 -1
- data/app/controllers/motor/forms_controller.rb +2 -0
- data/app/controllers/motor/queries_controller.rb +2 -0
- data/app/controllers/motor/resources_controller.rb +2 -0
- data/app/controllers/motor/run_queries_controller.rb +2 -0
- data/app/controllers/motor/send_alerts_controller.rb +2 -0
- data/app/models/motor/alert.rb +1 -1
- data/app/models/motor/alert_lock.rb +1 -1
- data/app/models/motor/config.rb +1 -1
- data/app/models/motor/dashboard.rb +1 -1
- data/app/models/motor/form.rb +1 -1
- data/app/models/motor/query.rb +1 -1
- data/app/models/motor/resource.rb +1 -1
- data/app/models/motor/tag.rb +1 -1
- data/app/models/motor/taggable_tag.rb +1 -1
- data/config/routes.rb +1 -0
- data/lib/motor.rb +1 -0
- data/lib/motor/active_record_utils.rb +2 -0
- data/lib/motor/active_record_utils/active_storage_links_extension.rb +15 -0
- data/lib/motor/active_record_utils/defined_scopes_extension.rb +19 -0
- data/lib/motor/admin.rb +14 -3
- data/lib/motor/alerts/scheduled_alerts_cache.rb +0 -4
- data/lib/motor/api_query.rb +2 -0
- data/lib/motor/api_query/apply_scope.rb +26 -0
- data/lib/motor/api_query/build_json.rb +3 -2
- data/lib/motor/api_query/sort.rb +28 -8
- data/lib/motor/build_schema.rb +60 -7
- data/lib/motor/build_schema/active_storage_attachment_schema.rb +84 -0
- data/lib/motor/build_schema/load_from_rails.rb +93 -83
- data/lib/motor/build_schema/merge_schema_configs.rb +13 -6
- data/lib/motor/build_schema/persist_resource_configs.rb +38 -1
- data/lib/motor/build_schema/reorder_schema.rb +25 -3
- data/lib/motor/queries/run_query.rb +10 -5
- data/lib/motor/version.rb +1 -1
- data/ui/dist/main-729641083f64367ce9aa.css.gz +0 -0
- data/ui/dist/main-729641083f64367ce9aa.js.gz +0 -0
- data/ui/dist/manifest.json +2 -5
- metadata +9 -4
- data/ui/dist/main-4bdedd7fcff1351efaf6.css.gz +0 -0
- data/ui/dist/main-4bdedd7fcff1351efaf6.js.gz +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d4b6ae30f7e0382a7ff6b56e04ef3de41396a89e0cff6c2595af76677af8685c
|
4
|
+
data.tar.gz: 8a6b99fd18067c6b535db0746eaef258da807a57d493e816b2c6b38396d716f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d41941aa4ccefd944c9caa0403d25e91f08e1a6ca7bf3064d49f181a906c9fa052777543a113d0ea079c5d317595b34257a3d7b7ee4540ee79fce4c52e54d8b3
|
7
|
+
data.tar.gz: 5f224df7542462dd74fd22093688351d5edd3fdea2615d51df3aa3a33d5194d6917f28ffc2c53a0298fcdb9c2c56e88bb97d539b18116f81d08a355adac60458
|
@@ -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
|
@@ -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
|
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
|
data/app/models/motor/alert.rb
CHANGED
data/app/models/motor/config.rb
CHANGED
data/app/models/motor/form.rb
CHANGED
data/app/models/motor/query.rb
CHANGED
data/app/models/motor/tag.rb
CHANGED
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
@@ -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
|
-
|
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
|
-
|
9
|
-
|
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
|
data/lib/motor/api_query.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
72
|
+
elsif model.instance_methods.include?(field.to_sym)
|
72
73
|
acc['methods'] << field
|
73
74
|
end
|
74
75
|
end
|
data/lib/motor/api_query/sort.rb
CHANGED
@@ -7,19 +7,39 @@ module Motor
|
|
7
7
|
|
8
8
|
module_function
|
9
9
|
|
10
|
-
def call(rel,
|
11
|
-
return rel if
|
10
|
+
def call(rel, param)
|
11
|
+
return rel if param.blank?
|
12
12
|
|
13
|
-
|
13
|
+
arel_order = build_arel_order(rel.klass, param)
|
14
|
+
join_params = build_join_params(rel.klass, param)
|
14
15
|
|
15
|
-
rel.order(
|
16
|
+
rel.order(arel_order).left_joins(join_params)
|
16
17
|
end
|
17
18
|
|
18
|
-
def
|
19
|
-
param.split(',').each_with_object({}) do |field,
|
20
|
-
|
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
|
-
|
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/build_schema.rb
CHANGED
@@ -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
|
-
|
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:
|
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:
|
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
|
-
|
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:
|
110
|
-
display_name:
|
111
|
-
column_type:
|
112
|
-
access_type:
|
113
|
-
default_value: default_attrs[
|
114
|
-
validators: fetch_validators(model,
|
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.
|
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
|
-
|
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:
|
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
|
-
|
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 ||
|
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
|
-
|
62
|
-
|
63
|
-
|
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
Binary file
|
Binary file
|
data/ui/dist/manifest.json
CHANGED
@@ -5,9 +5,6 @@
|
|
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
|
9
|
-
"main
|
10
|
-
"main-4bdedd7fcff1351efaf6.js.gz": "main-4bdedd7fcff1351efaf6.js.gz",
|
11
|
-
"main.css": "main-4bdedd7fcff1351efaf6.css",
|
12
|
-
"main.js": "main-4bdedd7fcff1351efaf6.js"
|
8
|
+
"main.css": "main-80be7992f1d3b992e2dc.css",
|
9
|
+
"main.js": "main-80be7992f1d3b992e2dc.js"
|
13
10
|
}
|
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.
|
4
|
+
version: 0.1.15
|
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-
|
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-
|
212
|
-
- ui/dist/main-
|
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:
|
Binary file
|
Binary file
|