motor-admin 0.1.36 → 0.1.42
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/alerts_controller.rb +12 -6
- data/app/controllers/motor/configs_controller.rb +1 -0
- data/app/controllers/motor/dashboards_controller.rb +4 -0
- data/app/controllers/motor/forms_controller.rb +4 -0
- data/app/controllers/motor/queries_controller.rb +4 -0
- data/app/controllers/motor/resources_controller.rb +1 -0
- data/app/controllers/motor/ui_controller.rb +4 -0
- data/app/models/motor/alert.rb +3 -3
- data/app/models/motor/application_record.rb +10 -0
- data/app/models/motor/config.rb +1 -1
- data/app/models/motor/dashboard.rb +2 -2
- data/app/models/motor/form.rb +2 -2
- data/app/models/motor/query.rb +3 -2
- data/app/models/motor/resource.rb +1 -1
- data/app/views/motor/ui/show.html.erb +1 -1
- data/lib/generators/motor/templates/install.rb +13 -13
- data/lib/motor.rb +11 -4
- data/lib/motor/active_record_utils/defined_scopes_extension.rb +1 -1
- data/lib/motor/admin.rb +8 -0
- data/lib/motor/alerts/persistance.rb +17 -3
- data/lib/motor/alerts/scheduler.rb +1 -1
- data/lib/motor/api_query/apply_scope.rb +12 -5
- data/lib/motor/api_query/sort.rb +1 -1
- data/lib/motor/build_schema.rb +3 -3
- data/lib/motor/build_schema/find_display_column.rb +31 -29
- data/lib/motor/build_schema/load_from_rails.rb +18 -9
- data/lib/motor/build_schema/merge_schema_configs.rb +8 -4
- data/lib/motor/build_schema/reorder_schema.rb +10 -4
- data/lib/motor/configs.rb +17 -0
- data/lib/motor/configs/build_configs_hash.rb +83 -0
- data/lib/motor/configs/build_ui_app_tag.rb +71 -0
- data/lib/motor/configs/load_from_cache.rb +81 -0
- data/lib/motor/configs/sync_from_file.rb +36 -0
- data/lib/motor/configs/sync_from_hash.rb +126 -0
- data/lib/motor/configs/sync_middleware.rb +72 -0
- data/lib/motor/configs/sync_with_remote.rb +47 -0
- data/lib/motor/configs/write_to_file.rb +36 -0
- data/lib/motor/dashboards/persistance.rb +15 -5
- data/lib/motor/forms/persistance.rb +15 -5
- data/lib/motor/net_http_utils.rb +38 -0
- data/lib/motor/queries/persistance.rb +13 -3
- data/lib/motor/railtie.rb +11 -0
- data/lib/motor/tasks/motor.rake +37 -0
- data/lib/motor/version.rb +1 -1
- data/ui/dist/{main-358ea31cd7020f915067.css.gz → main-05401628fabd32884fa6.css.gz} +0 -0
- data/ui/dist/main-05401628fabd32884fa6.js.gz +0 -0
- data/ui/dist/manifest.json +5 -5
- metadata +16 -6
- data/lib/motor/audited_utils.rb +0 -15
- data/lib/motor/ui_configs.rb +0 -82
- data/ui/dist/main-358ea31cd7020f915067.js.gz +0 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module SyncFromFile
|
6
|
+
FILE_PATH = Motor::Configs::FILE_PATH
|
7
|
+
MUTEXT = Mutex.new
|
8
|
+
FILE_TIMESTAMPS_STORE = ActiveSupport::Cache::MemoryStore.new(size: 1.megabyte)
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def call(with_exception: false)
|
13
|
+
MUTEXT.synchronize do
|
14
|
+
file = Rails.root.join(FILE_PATH)
|
15
|
+
|
16
|
+
file_timestamp =
|
17
|
+
begin
|
18
|
+
file.ctime
|
19
|
+
rescue Errno::ENOENT
|
20
|
+
raise if with_exception
|
21
|
+
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
next unless file_timestamp
|
26
|
+
|
27
|
+
FILE_TIMESTAMPS_STORE.fetch(file_timestamp.to_s) do
|
28
|
+
Motor::Configs::SyncFromHash.call(
|
29
|
+
YAML.safe_load(file.read, permitted_classes: [Time, Date])
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module SyncFromHash
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(configs_hash)
|
9
|
+
return if configs_hash.blank?
|
10
|
+
|
11
|
+
configs_hash = configs_hash.with_indifferent_access
|
12
|
+
|
13
|
+
Motor::ApplicationRecord.transaction do
|
14
|
+
sync_queries(configs_hash)
|
15
|
+
sync_alerts(configs_hash)
|
16
|
+
sync_dashboards(configs_hash)
|
17
|
+
sync_forms(configs_hash)
|
18
|
+
sync_configs(configs_hash)
|
19
|
+
sync_resources(configs_hash)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def sync_queries(configs_hash)
|
24
|
+
sync_taggable(
|
25
|
+
Motor::Configs::LoadFromCache.load_queries,
|
26
|
+
configs_hash[:queries],
|
27
|
+
configs_hash[:file_version],
|
28
|
+
Motor::Queries::Persistance.method(:update_from_params!)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def sync_alerts(configs_hash)
|
33
|
+
sync_taggable(
|
34
|
+
Motor::Configs::LoadFromCache.load_alerts,
|
35
|
+
configs_hash[:alerts],
|
36
|
+
configs_hash[:file_version],
|
37
|
+
Motor::Alerts::Persistance.method(:update_from_params!)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def sync_forms(configs_hash)
|
42
|
+
sync_taggable(
|
43
|
+
Motor::Configs::LoadFromCache.load_forms,
|
44
|
+
configs_hash[:forms],
|
45
|
+
configs_hash[:file_version],
|
46
|
+
Motor::Forms::Persistance.method(:update_from_params!)
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def sync_dashboards(configs_hash)
|
51
|
+
sync_taggable(
|
52
|
+
Motor::Configs::LoadFromCache.load_dashboards,
|
53
|
+
configs_hash[:dashboards],
|
54
|
+
configs_hash[:file_version],
|
55
|
+
Motor::Dashboards::Persistance.method(:update_from_params!)
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def sync_configs(configs_hash)
|
60
|
+
configs_index = Motor::Configs::LoadFromCache.load_configs.index_by(&:key)
|
61
|
+
|
62
|
+
configs_hash[:configs].each do |attrs|
|
63
|
+
record = configs_index[attrs[:key]] || Motor::Config.new
|
64
|
+
|
65
|
+
next if record.updated_at && attrs[:updated_at] <= record.updated_at
|
66
|
+
|
67
|
+
record.update!(attrs)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def sync_resources(configs_hash)
|
72
|
+
resources_index = Motor::Configs::LoadFromCache.load_resources.index_by(&:name)
|
73
|
+
|
74
|
+
configs_hash[:resources].each do |attrs|
|
75
|
+
record = resources_index[attrs[:name]] || Motor::Resource.new
|
76
|
+
|
77
|
+
next if record.updated_at && attrs[:updated_at] <= record.updated_at
|
78
|
+
|
79
|
+
record.update!(attrs)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def sync_taggable(records, config_items, configs_timestamp, update_proc)
|
84
|
+
processed_records, create_items = update_taggable_items(records, config_items, update_proc)
|
85
|
+
|
86
|
+
create_taggable_items(create_items, records.klass, update_proc)
|
87
|
+
|
88
|
+
archive_taggable_items(records - processed_records, configs_timestamp)
|
89
|
+
|
90
|
+
ActiveRecord::Base.connection.reset_pk_sequence!(records.klass.table_name)
|
91
|
+
end
|
92
|
+
|
93
|
+
def update_taggable_items(records, config_items, update_proc)
|
94
|
+
record_ids_hash = records.index_by(&:id)
|
95
|
+
|
96
|
+
config_items.each_with_object([[], []]) do |attrs, (processed_acc, create_acc)|
|
97
|
+
record = record_ids_hash[attrs[:id]]
|
98
|
+
|
99
|
+
next create_acc << attrs unless record
|
100
|
+
|
101
|
+
processed_acc << record if record
|
102
|
+
|
103
|
+
next if record.updated_at >= attrs[:updated_at]
|
104
|
+
|
105
|
+
update_proc.call(record, attrs, force_replace: true)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def create_taggable_items(create_items, records_class, update_proc)
|
110
|
+
create_items.each do |attrs|
|
111
|
+
record = records_class.find_or_initialize_by(id: attrs[:id]).tap { |e| e.deleted_at = nil }
|
112
|
+
|
113
|
+
update_proc.call(record, attrs, force_replace: true)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def archive_taggable_items(records_to_remove, configs_timestamp)
|
118
|
+
records_to_remove.each do |record|
|
119
|
+
next if record.updated_at > configs_timestamp
|
120
|
+
|
121
|
+
record.update!(deleted_at: Time.current) if record.deleted_at.blank?
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
class SyncMiddleware
|
6
|
+
KeyNotSpecified = Class.new(StandardError)
|
7
|
+
NotAuthenticated = Class.new(StandardError)
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
if env['PATH_INFO'] == Motor::Configs::SYNC_API_PATH
|
15
|
+
authenticate!(env['HTTP_X_AUTHORIZATION'])
|
16
|
+
|
17
|
+
case env['REQUEST_METHOD']
|
18
|
+
when 'GET'
|
19
|
+
respond_with_configs
|
20
|
+
when 'POST'
|
21
|
+
input = env['rack.input']
|
22
|
+
input.rewind
|
23
|
+
sync_configs(input.read)
|
24
|
+
else
|
25
|
+
@app.call(env)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
@app.call(env)
|
29
|
+
end
|
30
|
+
rescue NotAuthenticated
|
31
|
+
[403, {}, ['Invalid synchronization API key']]
|
32
|
+
rescue KeyNotSpecified
|
33
|
+
[404, {}, ['Set `MOTOR_SYNC_API_KEY` environment variable in order to sync configs']]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def authenticate!(token)
|
39
|
+
raise KeyNotSpecified if Motor::Configs::SYNC_ACCESS_KEY.blank?
|
40
|
+
raise NotAuthenticated if token.blank?
|
41
|
+
|
42
|
+
is_token_valid =
|
43
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
44
|
+
Digest::SHA256.hexdigest(token),
|
45
|
+
Digest::SHA256.hexdigest(Motor::Configs::SYNC_ACCESS_KEY)
|
46
|
+
)
|
47
|
+
|
48
|
+
raise NotAuthenticated unless is_token_valid
|
49
|
+
end
|
50
|
+
|
51
|
+
def respond_with_configs
|
52
|
+
[
|
53
|
+
200,
|
54
|
+
{ 'Content-Type' => 'application/json' },
|
55
|
+
[Motor::Configs::BuildConfigsHash.call.to_json]
|
56
|
+
]
|
57
|
+
rescue StandardError => e
|
58
|
+
[500, {}, [e.message]]
|
59
|
+
end
|
60
|
+
|
61
|
+
def sync_configs(body)
|
62
|
+
configs_hash = JSON.parse(body)
|
63
|
+
|
64
|
+
Motor::Configs::SyncFromHash.call(configs_hash)
|
65
|
+
|
66
|
+
[200, {}, []]
|
67
|
+
rescue StandardError => e
|
68
|
+
[500, {}, [e.message]]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module SyncWithRemote
|
6
|
+
UnableToSync = Class.new(StandardError)
|
7
|
+
ApiNotFound = Class.new(StandardError)
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def call(remote_url, api_key)
|
12
|
+
url = remote_url.sub(%r{/\z}, '') + Motor::Configs::SYNC_API_PATH
|
13
|
+
|
14
|
+
sync_from_remote!(url, api_key)
|
15
|
+
sync_to_remote!(url, api_key)
|
16
|
+
end
|
17
|
+
|
18
|
+
def sync_from_remote!(remote_url, api_key)
|
19
|
+
response = Motor::NetHttpUtils.get(remote_url, {}, { 'X-Authorization' => api_key })
|
20
|
+
|
21
|
+
raise ApiNotFound if response.is_a?(Net::HTTPNotFound)
|
22
|
+
raise UnableToSync, [response.message, response.body].join(': ') unless response.is_a?(Net::HTTPSuccess)
|
23
|
+
|
24
|
+
configs_hash = JSON.parse(response.body)
|
25
|
+
|
26
|
+
Motor::Configs::SyncFromHash.call(configs_hash)
|
27
|
+
end
|
28
|
+
|
29
|
+
def sync_to_remote!(remote_url, api_key)
|
30
|
+
configs_hash = Motor::Configs::BuildConfigsHash.call
|
31
|
+
|
32
|
+
response = Motor::NetHttpUtils.post(
|
33
|
+
remote_url,
|
34
|
+
{},
|
35
|
+
{
|
36
|
+
'X-Authorization' => api_key,
|
37
|
+
'Content-Type' => 'application/json'
|
38
|
+
},
|
39
|
+
configs_hash.to_json
|
40
|
+
)
|
41
|
+
|
42
|
+
raise ApiNotFound if response.is_a?(Net::HTTPNotFound)
|
43
|
+
raise UnableToSync, [response.message, response.body].join(': ') unless response.is_a?(Net::HTTPSuccess)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module WriteToFile
|
6
|
+
THREAD_POOL = Concurrent::FixedThreadPool.new(1)
|
7
|
+
FILE_PATH = Motor::Configs::FILE_PATH
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def call
|
12
|
+
return if THREAD_POOL.queue_length.positive?
|
13
|
+
|
14
|
+
THREAD_POOL.post do
|
15
|
+
ActiveRecord::Base.logger.silence do
|
16
|
+
write_with_lock
|
17
|
+
end
|
18
|
+
rescue StandardError => e
|
19
|
+
Rails.logger.error(e)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def write_with_lock
|
24
|
+
File.open(Rails.root.join(FILE_PATH), 'w') do |file|
|
25
|
+
file.flock(File::LOCK_EX)
|
26
|
+
|
27
|
+
YAML.dump(Motor::Configs::BuildConfigsHash.call, file)
|
28
|
+
|
29
|
+
file.flock(File::LOCK_UN)
|
30
|
+
|
31
|
+
file
|
32
|
+
end.close
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -29,16 +29,20 @@ module Motor
|
|
29
29
|
retry
|
30
30
|
end
|
31
31
|
|
32
|
-
def update_from_params!(dashboard, params)
|
32
|
+
def update_from_params!(dashboard, params, force_replace: false)
|
33
|
+
tag_ids = dashboard.tags.ids
|
34
|
+
|
33
35
|
dashboard = assign_attributes(dashboard, params)
|
34
36
|
|
35
|
-
raise TitleAlreadyExists if title_already_exists?(dashboard)
|
37
|
+
raise TitleAlreadyExists if !force_replace && title_already_exists?(dashboard)
|
36
38
|
|
37
39
|
ApplicationRecord.transaction do
|
40
|
+
archive_with_existing_name(dashboard) if force_replace
|
41
|
+
|
38
42
|
dashboard.save!
|
39
43
|
end
|
40
44
|
|
41
|
-
dashboard.tags.reload
|
45
|
+
dashboard.touch if tag_ids.sort != dashboard.tags.reload.ids.sort && params[:updated_at].blank?
|
42
46
|
|
43
47
|
dashboard
|
44
48
|
rescue ActiveRecord::RecordNotUnique
|
@@ -47,15 +51,21 @@ module Motor
|
|
47
51
|
|
48
52
|
def assign_attributes(dashboard, params)
|
49
53
|
dashboard.assign_attributes(params.slice(:title, :description, :preferences))
|
54
|
+
dashboard.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
|
50
55
|
|
51
56
|
Motor::Tags.assign_tags(dashboard, params[:tags])
|
52
57
|
end
|
53
58
|
|
59
|
+
def archive_with_existing_name(dashboard)
|
60
|
+
Motor::Dashboard.where(['lower(title) = ? AND id != ?', dashboard.title.to_s.downcase, dashboard.id])
|
61
|
+
.update_all(deleted_at: Time.current)
|
62
|
+
end
|
63
|
+
|
54
64
|
def title_already_exists?(dashboard)
|
55
65
|
if dashboard.new_record?
|
56
|
-
Dashboard.exists?(['lower(title) = ?', dashboard.title.to_s.downcase])
|
66
|
+
Motor::Dashboard.exists?(['lower(title) = ?', dashboard.title.to_s.downcase])
|
57
67
|
else
|
58
|
-
Dashboard.exists?(['lower(title) = ? AND id != ?', dashboard.title.to_s.downcase, dashboard.id])
|
68
|
+
Motor::Dashboard.exists?(['lower(title) = ? AND id != ?', dashboard.title.to_s.downcase, dashboard.id])
|
59
69
|
end
|
60
70
|
end
|
61
71
|
end
|
@@ -29,16 +29,20 @@ module Motor
|
|
29
29
|
retry
|
30
30
|
end
|
31
31
|
|
32
|
-
def update_from_params!(form, params)
|
32
|
+
def update_from_params!(form, params, force_replace: false)
|
33
|
+
tag_ids = form.tags.ids
|
34
|
+
|
33
35
|
form = assign_attributes(form, params)
|
34
36
|
|
35
|
-
raise NameAlreadyExists if name_already_exists?(form)
|
37
|
+
raise NameAlreadyExists if !force_replace && name_already_exists?(form)
|
36
38
|
|
37
39
|
ApplicationRecord.transaction do
|
40
|
+
archive_with_existing_name(form) if force_replace
|
41
|
+
|
38
42
|
form.save!
|
39
43
|
end
|
40
44
|
|
41
|
-
form.tags.reload
|
45
|
+
form.touch if tag_ids.sort != form.tags.reload.ids.sort && params[:updated_at].blank?
|
42
46
|
|
43
47
|
form
|
44
48
|
rescue ActiveRecord::RecordNotUnique
|
@@ -47,15 +51,21 @@ module Motor
|
|
47
51
|
|
48
52
|
def assign_attributes(form, params)
|
49
53
|
form.assign_attributes(params.slice(:name, :description, :api_path, :http_method, :preferences))
|
54
|
+
form.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
|
50
55
|
|
51
56
|
Motor::Tags.assign_tags(form, params[:tags])
|
52
57
|
end
|
53
58
|
|
59
|
+
def archive_with_existing_name(form)
|
60
|
+
Motor::Form.where(['lower(name) = ? AND id != ?', form.name.to_s.downcase, form.id])
|
61
|
+
.update_all(deleted_at: Time.current)
|
62
|
+
end
|
63
|
+
|
54
64
|
def name_already_exists?(form)
|
55
65
|
if form.new_record?
|
56
|
-
Form.exists?(['lower(name) = ?', form.name.to_s.downcase])
|
66
|
+
Motor::Form.exists?(['lower(name) = ?', form.name.to_s.downcase])
|
57
67
|
else
|
58
|
-
Form.exists?(['lower(name) = ? AND id != ?', form.name.to_s.downcase, form.id])
|
68
|
+
Motor::Form.exists?(['lower(name) = ? AND id != ?', form.name.to_s.downcase, form.id])
|
59
69
|
end
|
60
70
|
end
|
61
71
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module NetHttpUtils
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def get(url, params = {}, headers = {})
|
8
|
+
request = build_request(Net::HTTP::Get, url, params, headers, nil)
|
9
|
+
|
10
|
+
execute_request(request)
|
11
|
+
end
|
12
|
+
|
13
|
+
def post(url, params = {}, headers = {}, body = '')
|
14
|
+
request = build_request(Net::HTTP::Post, url, params, headers, body)
|
15
|
+
|
16
|
+
execute_request(request)
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_request(method_class, url, params, headers, body)
|
20
|
+
uri = URI(url)
|
21
|
+
uri.query = params.to_query
|
22
|
+
|
23
|
+
request = method_class.new(uri)
|
24
|
+
request.body = body if body.present?
|
25
|
+
headers.each { |key, value| request[key] = value }
|
26
|
+
|
27
|
+
request
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute_request(request)
|
31
|
+
uri = request.uri
|
32
|
+
|
33
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.port == 443) do |http|
|
34
|
+
http.request(request)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|