motor-admin 0.1.21 → 0.1.28
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 +6 -1
- data/app/views/layouts/motor/application.html.erb +1 -1
- data/lib/motor/admin.rb +15 -0
- data/lib/motor/api_query/build_json.rb +17 -10
- data/lib/motor/api_query/sort.rb +5 -1
- data/lib/motor/build_schema/load_from_rails.rb +48 -34
- data/lib/motor/build_schema/merge_schema_configs.rb +61 -14
- data/lib/motor/build_schema/persist_resource_configs.rb +49 -46
- data/lib/motor/build_schema/reorder_schema.rb +22 -14
- data/lib/motor/queries.rb +1 -0
- data/lib/motor/queries/render_sql_template.rb +51 -0
- data/lib/motor/queries/run_query.rb +50 -22
- data/lib/motor/ui_configs.rb +33 -13
- data/lib/motor/version.rb +1 -1
- data/ui/dist/main-4da1a5102d7bc66aefd0.css.gz +0 -0
- data/ui/dist/main-4da1a5102d7bc66aefd0.js.gz +0 -0
- data/ui/dist/manifest.json +5 -5
- metadata +5 -4
- data/ui/dist/main-015cc9721345d5b8af80.css.gz +0 -0
- data/ui/dist/main-015cc9721345d5b8af80.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: 5799ce999fa943638fa7c4d655400c1418ee80977bf47f64a71e6d5c496d02de
|
4
|
+
data.tar.gz: da99ec7b1c48708d983b663a7f9cb79b6d0e270b2fc9f5538a8121cfabdbc209
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5fc31a79b01ad0e716700a7c3daed980b293d13081294d1013918cce6f4d24cf4c104e0c44144cde225fd3b1ce07253f9b3ddaf44f1eca2252a01ff9124c52e
|
7
|
+
data.tar.gz: efca3a6d85ce6e33c83a63f3cd5addbd75b2baf099bd96c5efbac40445520a7725d68a5c3d0564792f06bdf12fed8958b13a6189047303d3c77f7f0af0ff0bd4
|
@@ -7,7 +7,7 @@ module Motor
|
|
7
7
|
load_and_authorize_resource :attachment, class: 'ActiveStorage::Attachment', parent: false
|
8
8
|
|
9
9
|
def create
|
10
|
-
if
|
10
|
+
if attachable?(@attachment.record)
|
11
11
|
@attachment.record.public_send(@attachment.name).attach(
|
12
12
|
io: StringIO.new(params.dig(:data, :file, :io).to_s.encode('ISO-8859-1')),
|
13
13
|
filename: params.dig(:data, :file, :filename)
|
@@ -21,6 +21,11 @@ module Motor
|
|
21
21
|
|
22
22
|
private
|
23
23
|
|
24
|
+
def attachable?(record)
|
25
|
+
record.respond_to?("#{@attachment.name}_attachment=") ||
|
26
|
+
record.respond_to?("#{@attachment.name}_attachments=")
|
27
|
+
end
|
28
|
+
|
24
29
|
def attachment_params
|
25
30
|
params.require(:data).except(:file).permit!
|
26
31
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<html>
|
3
3
|
<head>
|
4
4
|
<title>Motor Admin</title>
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
6
6
|
<link rel="stylesheet" media="all" href="<%= Motor::Assets.asset_path('main.css') %>">
|
7
7
|
<%= csrf_meta_tags %>
|
8
8
|
<%= csp_meta_tag %>
|
data/lib/motor/admin.rb
CHANGED
@@ -14,6 +14,21 @@ module Motor
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
initializer 'motor.basic_auth' do
|
18
|
+
next if ENV['MOTOR_AUTH_PASSWORD'].blank?
|
19
|
+
|
20
|
+
config.middleware.use Rack::Auth::Basic do |username, password|
|
21
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
22
|
+
::Digest::SHA256.hexdigest(username),
|
23
|
+
::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_USERNAME'].to_s)
|
24
|
+
) &
|
25
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
26
|
+
::Digest::SHA256.hexdigest(password),
|
27
|
+
::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_PASSWORD'].to_s)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
17
32
|
initializer 'motor.active_storage.extensions' do
|
18
33
|
ActiveSupport.on_load(:active_storage_attachment) do
|
19
34
|
ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
|
@@ -29,9 +29,11 @@ module Motor
|
|
29
29
|
hash = {}
|
30
30
|
|
31
31
|
path.split('.').reduce(hash) do |acc, part|
|
32
|
-
|
32
|
+
acc_hash = {}
|
33
33
|
|
34
|
-
acc[part]
|
34
|
+
acc[part] = acc_hash
|
35
|
+
|
36
|
+
acc_hash
|
35
37
|
end
|
36
38
|
|
37
39
|
accumulator.deep_merge(hash)
|
@@ -45,20 +47,25 @@ module Motor
|
|
45
47
|
return if params[:fields].blank?
|
46
48
|
|
47
49
|
model = rel.is_a?(ActiveRecord::Relation) ? rel.klass : rel.class
|
48
|
-
model_name = model.name.underscore
|
49
50
|
|
50
51
|
params[:fields].each do |key, fields|
|
51
52
|
fields = fields.split(',') if fields.is_a?(String)
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
hash = find_key_in_params(json_params, key)
|
54
|
+
merge_fields_params!(key, fields, json_params, model)
|
55
|
+
end
|
56
|
+
end
|
57
57
|
|
58
|
-
|
58
|
+
def merge_fields_params!(key, fields, json_params, model)
|
59
|
+
model_name = model.name.underscore
|
59
60
|
|
60
|
-
|
61
|
-
|
61
|
+
if key == model_name || model_name.split('/').last == key
|
62
|
+
json_params.merge!(build_fields_hash(model, fields))
|
63
|
+
else
|
64
|
+
hash = find_key_in_params(json_params, key)
|
65
|
+
|
66
|
+
fields_hash = build_fields_hash(model.reflections[key]&.klass, fields)
|
67
|
+
|
68
|
+
hash.merge!(fields_hash)
|
62
69
|
end
|
63
70
|
end
|
64
71
|
|
data/lib/motor/api_query/sort.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
module Motor
|
4
4
|
module BuildSchema
|
5
5
|
module LoadFromRails
|
6
|
+
MUTEX = Mutex.new
|
7
|
+
|
6
8
|
module_function
|
7
9
|
|
8
10
|
def call
|
@@ -82,21 +84,26 @@ module Motor
|
|
82
84
|
model.columns.map do |column|
|
83
85
|
next if reference_columns.find { |c| c[:name] == column.name }
|
84
86
|
|
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
|
-
}
|
87
|
+
build_table_column(column, model, default_attrs)
|
95
88
|
end.compact
|
96
89
|
|
97
90
|
reference_columns + table_columns
|
98
91
|
end
|
99
92
|
|
93
|
+
def build_table_column(column, model, default_attrs)
|
94
|
+
{
|
95
|
+
name: column.name,
|
96
|
+
display_name: column.name.humanize,
|
97
|
+
column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
|
98
|
+
access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
|
99
|
+
default_value: default_attrs[column.name],
|
100
|
+
validators: fetch_validators(model, column.name),
|
101
|
+
reference: nil,
|
102
|
+
format: {},
|
103
|
+
virtual: false
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
100
107
|
def fetch_reference_columns(model)
|
101
108
|
default_attrs = model.new.attributes
|
102
109
|
|
@@ -109,31 +116,36 @@ module Motor
|
|
109
116
|
next
|
110
117
|
end
|
111
118
|
|
112
|
-
column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
|
113
|
-
|
114
119
|
next if ref.klass.name == 'ActiveStorage::Blob'
|
115
120
|
|
116
|
-
|
117
|
-
|
118
|
-
{
|
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
|
-
},
|
132
|
-
virtual: false
|
133
|
-
}
|
121
|
+
build_reflection_column(name, model, ref, default_attrs)
|
134
122
|
end.compact
|
135
123
|
end
|
136
124
|
|
125
|
+
def build_reflection_column(name, model, ref, default_attrs)
|
126
|
+
column_name = ref.belongs_to? ? ref.foreign_key.to_s : name
|
127
|
+
is_attachment = ref.klass.name == 'ActiveStorage::Attachment'
|
128
|
+
access_type = ref.belongs_to? || is_attachment ? ColumnAccessTypes::READ_WRITE : ColumnAccessTypes::READ_ONLY
|
129
|
+
|
130
|
+
{
|
131
|
+
name: column_name,
|
132
|
+
display_name: column_name.humanize,
|
133
|
+
column_type: is_attachment ? 'file' : 'integer',
|
134
|
+
access_type: access_type,
|
135
|
+
default_value: default_attrs[column_name],
|
136
|
+
validators: fetch_validators(model, column_name),
|
137
|
+
format: {},
|
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
|
+
},
|
145
|
+
virtual: false
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
137
149
|
def fetch_associations(model)
|
138
150
|
model.reflections.map do |name, ref|
|
139
151
|
next if ref.has_one? || ref.belongs_to?
|
@@ -180,10 +192,12 @@ module Motor
|
|
180
192
|
end
|
181
193
|
|
182
194
|
def eager_load_models!
|
183
|
-
|
184
|
-
Zeitwerk::Loader
|
185
|
-
|
186
|
-
|
195
|
+
MUTEX.synchronize do
|
196
|
+
if Rails::VERSION::MAJOR > 5 && defined?(Zeitwerk::Loader)
|
197
|
+
Zeitwerk::Loader.eager_load_all
|
198
|
+
else
|
199
|
+
Rails.application.eager_load!
|
200
|
+
end
|
187
201
|
end
|
188
202
|
end
|
189
203
|
end
|
@@ -27,50 +27,97 @@ module Motor
|
|
27
27
|
def merge_model(model, configs)
|
28
28
|
updated_model = model.merge(configs.slice(*RESOURCE_ATTRS))
|
29
29
|
|
30
|
-
updated_model
|
30
|
+
merge_actions!(updated_model, configs)
|
31
|
+
merge_assiciations!(updated_model, configs)
|
32
|
+
merge_columns!(updated_model, configs)
|
33
|
+
merge_tabs!(updated_model, configs)
|
34
|
+
merge_scopes!(updated_model, configs)
|
35
|
+
|
36
|
+
updated_model
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param model [HashWithIndifferentAccess]
|
40
|
+
# @param configs [HashWithIndifferentAccess]
|
41
|
+
# @return [HashWithIndifferentAccess]
|
42
|
+
def merge_assiciations!(model, configs)
|
43
|
+
model[:associations] = merge_by_name(
|
31
44
|
model[:associations],
|
32
|
-
configs[:associations]
|
45
|
+
configs[:associations],
|
46
|
+
{},
|
47
|
+
->(_) { true }
|
33
48
|
)
|
34
49
|
|
35
|
-
|
50
|
+
model
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param model [HashWithIndifferentAccess]
|
54
|
+
# @param configs [HashWithIndifferentAccess]
|
55
|
+
# @return [HashWithIndifferentAccess]
|
56
|
+
def merge_columns!(model, configs)
|
57
|
+
model[:columns] = merge_by_name(
|
36
58
|
model[:columns],
|
37
59
|
configs[:columns],
|
38
|
-
COLUMN_DEFAULTS
|
60
|
+
COLUMN_DEFAULTS,
|
61
|
+
->(scope) { !scope[:virtual] }
|
39
62
|
)
|
40
63
|
|
41
|
-
|
64
|
+
model
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param model [HashWithIndifferentAccess]
|
68
|
+
# @param configs [HashWithIndifferentAccess]
|
69
|
+
# @return [HashWithIndifferentAccess]
|
70
|
+
def merge_actions!(model, configs)
|
71
|
+
model[:actions] = merge_by_name(
|
42
72
|
model[:actions],
|
43
73
|
configs[:actions],
|
44
74
|
ACTION_DEFAULTS
|
45
75
|
)
|
46
76
|
|
47
|
-
|
77
|
+
model
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param model [HashWithIndifferentAccess]
|
81
|
+
# @param configs [HashWithIndifferentAccess]
|
82
|
+
# @return [HashWithIndifferentAccess]
|
83
|
+
def merge_tabs!(model, configs)
|
84
|
+
model[:tabs] = merge_by_name(
|
48
85
|
model[:tabs],
|
49
86
|
configs[:tabs],
|
50
87
|
TAB_DEFAULTS
|
51
88
|
)
|
52
89
|
|
53
|
-
|
90
|
+
model
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param model [HashWithIndifferentAccess]
|
94
|
+
# @param configs [HashWithIndifferentAccess]
|
95
|
+
# @return [HashWithIndifferentAccess]
|
96
|
+
def merge_scopes!(model, configs)
|
97
|
+
model[:scopes] = merge_by_name(
|
54
98
|
model[:scopes],
|
55
99
|
configs[:scopes],
|
56
|
-
SCOPE_DEFAULTS
|
100
|
+
SCOPE_DEFAULTS,
|
101
|
+
->(scope) { scope[:scope_type] != 'filter' }
|
57
102
|
)
|
58
103
|
|
59
|
-
|
104
|
+
model
|
60
105
|
end
|
61
106
|
|
62
107
|
# @param defaults [Array<HashWithIndifferentAccess>]
|
63
108
|
# @param configs [Array<HashWithIndifferentAccess>]
|
64
109
|
# @return [Array<HashWithIndifferentAccess>]
|
65
|
-
def merge_by_name(defaults, configs, default_attrs = {})
|
110
|
+
def merge_by_name(defaults, configs, default_attrs = {}, default_item_check = nil)
|
66
111
|
return defaults if configs.blank?
|
67
112
|
|
68
113
|
(defaults.pluck(:name) + configs.pluck(:name)).uniq.map do |name|
|
69
|
-
config_item = configs.find { |e| e[:name] == name }
|
70
|
-
default_item = defaults.find { |e| e[:name] == name }
|
114
|
+
config_item = configs.find { |e| e[:name] == name }
|
115
|
+
default_item = defaults.find { |e| e[:name] == name }
|
71
116
|
|
72
|
-
default_item.
|
73
|
-
|
117
|
+
next if default_item.nil? && default_item_check&.call(config_item)
|
118
|
+
|
119
|
+
(default_item || default_attrs).merge(config_item || {})
|
120
|
+
end.compact
|
74
121
|
end
|
75
122
|
|
76
123
|
# @return [HashWithIndifferentAccess<String, HashWithIndifferentAccess>]
|
@@ -4,7 +4,7 @@ module Motor
|
|
4
4
|
module BuildSchema
|
5
5
|
module PersistResourceConfigs
|
6
6
|
RESOURCE_ATTRS = %w[display_name visible].freeze
|
7
|
-
COLUMN_ATTRS = %w[name display_name column_type access_type default_value virtual].freeze
|
7
|
+
COLUMN_ATTRS = %w[name display_name column_type access_type default_value virtual format].freeze
|
8
8
|
ASSOCIATION_ATTRS = %w[name display_name visible].freeze
|
9
9
|
SCOPE_ATTRS = %w[name display_name scope_type preferences visible].freeze
|
10
10
|
ACTION_ATTRS = %w[name display_name action_type preferences visible].freeze
|
@@ -14,6 +14,7 @@ module Motor
|
|
14
14
|
access_type: 'read_write',
|
15
15
|
default_value: nil,
|
16
16
|
reference: nil,
|
17
|
+
format: {},
|
17
18
|
validators: []
|
18
19
|
}.with_indifferent_access
|
19
20
|
|
@@ -24,6 +25,7 @@ module Motor
|
|
24
25
|
|
25
26
|
TAB_DEFAULTS = {
|
26
27
|
visible: true,
|
28
|
+
tab_type: 'default',
|
27
29
|
preferences: {}
|
28
30
|
}.with_indifferent_access
|
29
31
|
|
@@ -75,47 +77,26 @@ module Motor
|
|
75
77
|
normalized_preferences = existing_prefs.merge(normalized_preferences)
|
76
78
|
normalized_preferences = reject_default(default_prefs, normalized_preferences)
|
77
79
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
)
|
84
|
-
end
|
80
|
+
normalize_configs!(normalized_preferences, :columns, default_prefs, existing_prefs, new_prefs)
|
81
|
+
normalize_configs!(normalized_preferences, :associations, default_prefs, existing_prefs, new_prefs)
|
82
|
+
normalize_configs!(normalized_preferences, :actions, default_prefs, existing_prefs, new_prefs)
|
83
|
+
normalize_configs!(normalized_preferences, :tabs, default_prefs, existing_prefs, new_prefs)
|
84
|
+
normalize_configs!(normalized_preferences, :scopes, default_prefs, existing_prefs, new_prefs)
|
85
85
|
|
86
|
-
|
87
|
-
|
88
|
-
default_prefs[:associations],
|
89
|
-
existing_prefs.fetch(:associations, []),
|
90
|
-
new_prefs.fetch(:associations, [])
|
91
|
-
)
|
92
|
-
end
|
86
|
+
normalized_preferences.compact
|
87
|
+
end
|
93
88
|
|
94
|
-
|
95
|
-
|
96
|
-
default_prefs[:actions],
|
97
|
-
existing_prefs.fetch(:actions, []),
|
98
|
-
new_prefs.fetch(:actions, [])
|
99
|
-
)
|
100
|
-
end
|
89
|
+
def normalize_configs!(preferences, configs_name, default_prefs, existing_prefs, new_prefs)
|
90
|
+
return preferences if new_prefs[configs_name].blank?
|
101
91
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
new_prefs.fetch(:tabs, [])
|
107
|
-
)
|
108
|
-
end
|
92
|
+
normalized_configs = public_send("normalize_#{configs_name}",
|
93
|
+
default_prefs[:actions],
|
94
|
+
existing_prefs.fetch(:actions, []),
|
95
|
+
new_prefs.fetch(:actions, []))
|
109
96
|
|
110
|
-
|
111
|
-
normalized_preferences[:scopes] = normalize_scopes(
|
112
|
-
default_prefs[:scopes],
|
113
|
-
existing_prefs.fetch(:scopes, []),
|
114
|
-
new_prefs.fetch(:scopes, [])
|
115
|
-
)
|
116
|
-
end
|
97
|
+
preferences[configs_name] = normalized_configs
|
117
98
|
|
118
|
-
|
99
|
+
preferences
|
119
100
|
end
|
120
101
|
|
121
102
|
# @param default_columns [Array<HashWithIndifferentAccess>]
|
@@ -123,7 +104,7 @@ module Motor
|
|
123
104
|
# @param new_columns [Array<HashWithIndifferentAccess>]
|
124
105
|
# @return [Array<HashWithIndifferentAccess>]
|
125
106
|
def normalize_columns(default_columns, existing_columns, new_columns)
|
126
|
-
(existing_columns
|
107
|
+
fetch_update_names(existing_columns, new_columns).uniq.map do |name|
|
127
108
|
new_column = safe_fetch_by_name(new_columns, name)
|
128
109
|
|
129
110
|
next if new_column[:_remove]
|
@@ -135,7 +116,11 @@ module Motor
|
|
135
116
|
normalized_column = existing_column.merge(column_attrs)
|
136
117
|
normalized_column = reject_default(default_column, normalized_column)
|
137
118
|
|
138
|
-
|
119
|
+
next if normalized_column.blank?
|
120
|
+
|
121
|
+
normalized_column[:name] ||= name
|
122
|
+
|
123
|
+
normalized_column
|
139
124
|
end.compact.presence
|
140
125
|
end
|
141
126
|
|
@@ -144,7 +129,7 @@ module Motor
|
|
144
129
|
# @param new_actions [Array<HashWithIndifferentAccess>]
|
145
130
|
# @return [Array<HashWithIndifferentAccess>]
|
146
131
|
def normalize_actions(default_actions, existing_actions, new_actions)
|
147
|
-
(existing_actions
|
132
|
+
fetch_update_names(existing_actions, new_actions).map do |name|
|
148
133
|
new_action = safe_fetch_by_name(new_actions, name)
|
149
134
|
|
150
135
|
next if new_action[:_remove]
|
@@ -156,7 +141,11 @@ module Motor
|
|
156
141
|
normalized_action = existing_action.merge(action_attrs)
|
157
142
|
normalized_action = reject_default(default_action.presence || ACTION_DEFAULTS, normalized_action)
|
158
143
|
|
159
|
-
|
144
|
+
next if normalized_action.blank?
|
145
|
+
|
146
|
+
normalized_action[:name] ||= name
|
147
|
+
|
148
|
+
normalized_action
|
160
149
|
end.compact.presence
|
161
150
|
end
|
162
151
|
|
@@ -165,7 +154,7 @@ module Motor
|
|
165
154
|
# @param new_tabs [Array<HashWithIndifferentAccess>]
|
166
155
|
# @return [Array<HashWithIndifferentAccess>]
|
167
156
|
def normalize_tabs(default_tabs, existing_tabs, new_tabs)
|
168
|
-
(existing_tabs
|
157
|
+
fetch_update_names(existing_tabs, new_tabs).uniq.map do |name|
|
169
158
|
new_tab = safe_fetch_by_name(new_tabs, name)
|
170
159
|
|
171
160
|
next if new_tab[:_remove]
|
@@ -177,7 +166,11 @@ module Motor
|
|
177
166
|
normalized_tab = existing_tab.merge(tab_attrs)
|
178
167
|
normalized_tab = reject_default(default_tab.presence || TAB_DEFAULTS, normalized_tab)
|
179
168
|
|
180
|
-
|
169
|
+
next if normalized_tab.blank?
|
170
|
+
|
171
|
+
normalized_tab[:name] ||= name
|
172
|
+
|
173
|
+
normalized_tab
|
181
174
|
end.compact.presence
|
182
175
|
end
|
183
176
|
|
@@ -186,7 +179,7 @@ module Motor
|
|
186
179
|
# @param new_scopes [Array<HashWithIndifferentAccess>]
|
187
180
|
# @return [Array<HashWithIndifferentAccess>]
|
188
181
|
def normalize_scopes(default_scopes, existing_scopes, new_scopes)
|
189
|
-
(existing_scopes
|
182
|
+
fetch_update_names(existing_scopes, new_scopes).uniq.map do |name|
|
190
183
|
new_scope = safe_fetch_by_name(new_scopes, name)
|
191
184
|
|
192
185
|
next if new_scope[:_remove]
|
@@ -198,7 +191,11 @@ module Motor
|
|
198
191
|
normalized_scope = existing_scope.merge(scope_attrs)
|
199
192
|
normalized_scope = reject_default(default_scope.presence || SCOPE_DEFAULTS, normalized_scope)
|
200
193
|
|
201
|
-
|
194
|
+
next if normalized_scope.blank?
|
195
|
+
|
196
|
+
normalized_scope[:name] ||= name
|
197
|
+
|
198
|
+
normalized_scope
|
202
199
|
end.compact.presence
|
203
200
|
end
|
204
201
|
|
@@ -220,8 +217,14 @@ module Motor
|
|
220
217
|
end.compact.presence
|
221
218
|
end
|
222
219
|
|
220
|
+
def fetch_update_names(existing_items, new_items)
|
221
|
+
new_names = new_items.map { |e| e[:_update] || e[:name] }
|
222
|
+
|
223
|
+
(existing_items.pluck(:name) + new_names).uniq
|
224
|
+
end
|
225
|
+
|
223
226
|
def safe_fetch_by_name(list, name)
|
224
|
-
list.find { |e| e[:name] == name } || {}
|
227
|
+
list.find { |e| e[:_update] == name || e[:name] == name } || {}
|
225
228
|
end
|
226
229
|
|
227
230
|
# @param resource_name [String]
|
@@ -23,21 +23,29 @@ module Motor
|
|
23
23
|
|
24
24
|
schema = sort_by_name(schema, configs['resources.order'])
|
25
25
|
|
26
|
-
schema.map
|
27
|
-
|
28
|
-
associations_order = configs["resources.#{model[:name]}.associations.order"]
|
29
|
-
actions_order = configs["resources.#{model[:name]}.actions.order"]
|
30
|
-
tabs_order = configs["resources.#{model[:name]}.tabs.order"]
|
31
|
-
scopes_order = configs["resources.#{model[:name]}.scopes.order"]
|
26
|
+
schema.map { |model| reorder_model(model, configs) }
|
27
|
+
end
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
)
|
40
|
-
|
29
|
+
def reorder_model(model, configs)
|
30
|
+
order_configs = build_order_configs(model[:name], configs)
|
31
|
+
|
32
|
+
model.merge(
|
33
|
+
columns: sort_by_name(sort_columns(model[:columns]), order_configs[:columns], sort_alphabetically: false),
|
34
|
+
associations: sort_by_name(model[:associations], order_configs[:associations]),
|
35
|
+
actions: sort_by_name(model[:actions], order_configs[:actions], sort_alphabetically: false),
|
36
|
+
tabs: sort_by_name(model[:tabs], order_configs[:tabs], sort_alphabetically: false),
|
37
|
+
scopes: sort_by_name(model[:scopes], order_configs[:scopes])
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_order_configs(model_name, configs)
|
42
|
+
{
|
43
|
+
columns: configs["resources.#{model_name}.columns.order"],
|
44
|
+
associations: configs["resources.#{model_name}.associations.order"],
|
45
|
+
actions: configs["resources.#{model_name}.actions.order"],
|
46
|
+
tabs: configs["resources.#{model_name}.tabs.order"],
|
47
|
+
scopes: configs["resources.#{model_name}.scopes.order"]
|
48
|
+
}
|
41
49
|
end
|
42
50
|
|
43
51
|
# @param list [Array<HashWithIndifferentAccess>]
|
data/lib/motor/queries.rb
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Queries
|
5
|
+
module RenderSqlTemplate
|
6
|
+
SECTION_OPEN_REGEXP = /{{([#^])\s*(\w+)}}.*\z/m.freeze
|
7
|
+
VARIABLE_REGEXP = /{{\s*(\w+)\s*}}/m.freeze
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def call(sql, variables)
|
12
|
+
result = render_sections(sql, variables)
|
13
|
+
|
14
|
+
interpolate_variables(result, variables)
|
15
|
+
end
|
16
|
+
|
17
|
+
def interpolate_variables(sql, variables)
|
18
|
+
selected_variables = []
|
19
|
+
|
20
|
+
rendered =
|
21
|
+
sql.gsub(VARIABLE_REGEXP) do
|
22
|
+
variable_name = Regexp.last_match[1]
|
23
|
+
|
24
|
+
index = selected_variables.index { |name, _| name == variable_name }
|
25
|
+
selected_variables << [variable_name, variables[variable_name]] unless index
|
26
|
+
|
27
|
+
"$#{selected_variables.size}"
|
28
|
+
end
|
29
|
+
|
30
|
+
[rendered, selected_variables]
|
31
|
+
end
|
32
|
+
|
33
|
+
def render_sections(sql, variables)
|
34
|
+
sql.sub(SECTION_OPEN_REGEXP) do |e|
|
35
|
+
variable_name = Regexp.last_match[2]
|
36
|
+
is_negative = Regexp.last_match[1] == '^'
|
37
|
+
|
38
|
+
_, content, rest = e.split(build_section_close_regexp(variable_name), 3)
|
39
|
+
|
40
|
+
is_present = variables[variable_name].present?
|
41
|
+
|
42
|
+
render_sections(is_present ^ is_negative ? content + rest.to_s : rest, variables)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_section_close_regexp(variable_name)
|
47
|
+
%r{{{[#^/]s*#{Regexp.escape(variable_name)}\s*}}}m
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -3,8 +3,7 @@
|
|
3
3
|
module Motor
|
4
4
|
module Queries
|
5
5
|
module RunQuery
|
6
|
-
DEFAULT_LIMIT =
|
7
|
-
INTERPOLATION_REGEXP = /{{(\w+)}}/.freeze
|
6
|
+
DEFAULT_LIMIT = 100_000
|
8
7
|
|
9
8
|
QueryResult = Struct.new(:data, :columns, keyword_init: true)
|
10
9
|
|
@@ -14,6 +13,10 @@ module Motor
|
|
14
13
|
|
15
14
|
module_function
|
16
15
|
|
16
|
+
# @param query [Motor::Query]
|
17
|
+
# @param variables_hash [Hash]
|
18
|
+
# @param limit [Integer]
|
19
|
+
# @return [Motor::Queries::RunQuery::QueryResult]
|
17
20
|
def call(query, variables_hash: nil, limit: DEFAULT_LIMIT)
|
18
21
|
variables_hash ||= {}
|
19
22
|
|
@@ -22,15 +25,27 @@ module Motor
|
|
22
25
|
QueryResult.new(data: result.rows, columns: build_columns_hash(result))
|
23
26
|
end
|
24
27
|
|
28
|
+
# @param query [Motor::Query]
|
29
|
+
# @param limit [Integer]
|
30
|
+
# @param variables_hash [Hash]
|
31
|
+
# @return [ActiveRecord::Result]
|
25
32
|
def execute_query(query, limit, variables_hash)
|
33
|
+
result = nil
|
26
34
|
statement = prepare_sql_statement(query, limit, variables_hash)
|
27
35
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
36
|
+
ActiveRecord::Base.transaction do
|
37
|
+
result =
|
38
|
+
case ActiveRecord::Base.connection
|
39
|
+
when ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
40
|
+
PostgresqlExecQuery.call(ActiveRecord::Base.connection, statement)
|
41
|
+
else
|
42
|
+
ActiveRecord::Base.connection.exec_query(*statement)
|
43
|
+
end
|
44
|
+
|
45
|
+
raise ActiveRecord::Rollback
|
33
46
|
end
|
47
|
+
|
48
|
+
result
|
34
49
|
end
|
35
50
|
|
36
51
|
# @param result [ActiveRecord::Result]
|
@@ -47,27 +62,40 @@ module Motor
|
|
47
62
|
end
|
48
63
|
end
|
49
64
|
|
65
|
+
# @param query [Motor::Query]
|
66
|
+
# @param limit [Integer]
|
67
|
+
# @param variables_hash [Hash]
|
68
|
+
# @return [Array]
|
50
69
|
def prepare_sql_statement(query, limit, variables_hash)
|
51
|
-
variables = query.preferences.fetch(:variables, [])
|
52
|
-
|
53
|
-
sql =
|
54
|
-
query.sql_body.gsub(INTERPOLATION_REGEXP) do
|
55
|
-
index = variables.index { |name, _| name == (Regexp.last_match[1]) } + 1
|
70
|
+
variables = merge_variable_default_values(query.preferences.fetch(:variables, []), variables_hash)
|
56
71
|
|
57
|
-
|
58
|
-
end
|
72
|
+
sql, query_variables = RenderSqlTemplate.call(query.sql_body, variables)
|
59
73
|
|
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
|
74
|
+
attributes = build_statement_attributes(query_variables)
|
68
75
|
|
69
76
|
[format(WITH_STATEMENT_TEMPLATE, sql_body: sql.strip.gsub(/;\z/, ''), limit: limit), 'SQL', attributes]
|
70
77
|
end
|
78
|
+
|
79
|
+
# @param variables [Array<(String, Object)>]
|
80
|
+
# @return [Array<ActiveRecord::Relation::QueryAttribute>]
|
81
|
+
def build_statement_attributes(variables)
|
82
|
+
variables.map do |variable_name, value|
|
83
|
+
ActiveRecord::Relation::QueryAttribute.new(
|
84
|
+
variable_name,
|
85
|
+
value,
|
86
|
+
ActiveRecord::Type::Value.new
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param variable_configs [Array<Hash>]
|
92
|
+
# @param variable_hash [Hash]
|
93
|
+
# @return [Hash]
|
94
|
+
def merge_variable_default_values(variable_configs, variables_hash)
|
95
|
+
variable_configs.each_with_object({}) do |variable, acc|
|
96
|
+
acc[variable[:name]] = variables_hash[variable[:name]] || variable[:default_value]
|
97
|
+
end
|
98
|
+
end
|
71
99
|
end
|
72
100
|
end
|
73
101
|
end
|
data/lib/motor/ui_configs.rb
CHANGED
@@ -27,22 +27,42 @@ module Motor
|
|
27
27
|
{
|
28
28
|
base_path: Motor::Admin.routes.url_helpers.motor_path,
|
29
29
|
schema: Motor::BuildSchema.call,
|
30
|
-
header_links:
|
31
|
-
queries:
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
.as_json(only: %i[id title updated_at],
|
36
|
-
include: { tags: { only: %i[id name] } }),
|
37
|
-
alerts: Motor::Alert.all.active.preload(:tags)
|
38
|
-
.as_json(only: %i[id name is_enabled updated_at],
|
39
|
-
include: { tags: { only: %i[id name] } }),
|
40
|
-
forms: Motor::Form.all.active.preload(:tags)
|
41
|
-
.as_json(only: %i[id name updated_at],
|
42
|
-
include: { tags: { only: %i[id name] } })
|
30
|
+
header_links: header_links_data_hash,
|
31
|
+
queries: queries_data_hash,
|
32
|
+
dashboards: dashboards_data_hash,
|
33
|
+
alerts: alerts_data_hash,
|
34
|
+
forms: forms_data_hash
|
43
35
|
}
|
44
36
|
end
|
45
37
|
|
38
|
+
def header_links_data_hash
|
39
|
+
Motor::Config.find_by(key: 'header.links')&.value || []
|
40
|
+
end
|
41
|
+
|
42
|
+
def queries_data_hash
|
43
|
+
Motor::Query.all.active.preload(:tags)
|
44
|
+
.as_json(only: %i[id name updated_at],
|
45
|
+
include: { tags: { only: %i[id name] } })
|
46
|
+
end
|
47
|
+
|
48
|
+
def dashboards_data_hash
|
49
|
+
Motor::Dashboard.all.active.preload(:tags)
|
50
|
+
.as_json(only: %i[id title updated_at],
|
51
|
+
include: { tags: { only: %i[id name] } })
|
52
|
+
end
|
53
|
+
|
54
|
+
def alerts_data_hash
|
55
|
+
Motor::Alert.all.active.preload(:tags)
|
56
|
+
.as_json(only: %i[id name is_enabled updated_at],
|
57
|
+
include: { tags: { only: %i[id name] } })
|
58
|
+
end
|
59
|
+
|
60
|
+
def forms_data_hash
|
61
|
+
Motor::Form.all.active.preload(:tags)
|
62
|
+
.as_json(only: %i[id name updated_at],
|
63
|
+
include: { tags: { only: %i[id name] } })
|
64
|
+
end
|
65
|
+
|
46
66
|
# @return [String]
|
47
67
|
def cache_key
|
48
68
|
ActiveRecord::Base.connection.execute(
|
data/lib/motor/version.rb
CHANGED
Binary file
|
Binary file
|
data/ui/dist/manifest.json
CHANGED
@@ -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-
|
9
|
-
"main-
|
10
|
-
"main-
|
11
|
-
"main.css": "main-
|
12
|
-
"main.js": "main-
|
8
|
+
"main-4da1a5102d7bc66aefd0.css.gz": "main-4da1a5102d7bc66aefd0.css.gz",
|
9
|
+
"main-4da1a5102d7bc66aefd0.js.LICENSE.txt": "main-4da1a5102d7bc66aefd0.js.LICENSE.txt",
|
10
|
+
"main-4da1a5102d7bc66aefd0.js.gz": "main-4da1a5102d7bc66aefd0.js.gz",
|
11
|
+
"main.css": "main-4da1a5102d7bc66aefd0.css",
|
12
|
+
"main.js": "main-4da1a5102d7bc66aefd0.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.
|
4
|
+
version: 0.1.28
|
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-05-
|
11
|
+
date: 2021-05-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord-filter
|
@@ -208,13 +208,14 @@ files:
|
|
208
208
|
- lib/motor/queries.rb
|
209
209
|
- lib/motor/queries/persistance.rb
|
210
210
|
- lib/motor/queries/postgresql_exec_query.rb
|
211
|
+
- lib/motor/queries/render_sql_template.rb
|
211
212
|
- lib/motor/queries/run_query.rb
|
212
213
|
- lib/motor/tags.rb
|
213
214
|
- lib/motor/ui_configs.rb
|
214
215
|
- lib/motor/version.rb
|
215
216
|
- ui/dist/fonts/ionicons.woff2
|
216
|
-
- ui/dist/main-
|
217
|
-
- ui/dist/main-
|
217
|
+
- ui/dist/main-4da1a5102d7bc66aefd0.css.gz
|
218
|
+
- ui/dist/main-4da1a5102d7bc66aefd0.js.gz
|
218
219
|
- ui/dist/manifest.json
|
219
220
|
homepage:
|
220
221
|
licenses:
|
Binary file
|
Binary file
|