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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/motor/alerts_controller.rb +12 -6
  3. data/app/controllers/motor/configs_controller.rb +1 -0
  4. data/app/controllers/motor/dashboards_controller.rb +4 -0
  5. data/app/controllers/motor/forms_controller.rb +4 -0
  6. data/app/controllers/motor/queries_controller.rb +4 -0
  7. data/app/controllers/motor/resources_controller.rb +1 -0
  8. data/app/controllers/motor/ui_controller.rb +4 -0
  9. data/app/models/motor/alert.rb +3 -3
  10. data/app/models/motor/application_record.rb +10 -0
  11. data/app/models/motor/config.rb +1 -1
  12. data/app/models/motor/dashboard.rb +2 -2
  13. data/app/models/motor/form.rb +2 -2
  14. data/app/models/motor/query.rb +3 -2
  15. data/app/models/motor/resource.rb +1 -1
  16. data/app/views/motor/ui/show.html.erb +1 -1
  17. data/lib/generators/motor/templates/install.rb +13 -13
  18. data/lib/motor.rb +11 -4
  19. data/lib/motor/active_record_utils/defined_scopes_extension.rb +1 -1
  20. data/lib/motor/admin.rb +8 -0
  21. data/lib/motor/alerts/persistance.rb +17 -3
  22. data/lib/motor/alerts/scheduler.rb +1 -1
  23. data/lib/motor/api_query/apply_scope.rb +12 -5
  24. data/lib/motor/api_query/sort.rb +1 -1
  25. data/lib/motor/build_schema.rb +3 -3
  26. data/lib/motor/build_schema/find_display_column.rb +31 -29
  27. data/lib/motor/build_schema/load_from_rails.rb +18 -9
  28. data/lib/motor/build_schema/merge_schema_configs.rb +8 -4
  29. data/lib/motor/build_schema/reorder_schema.rb +10 -4
  30. data/lib/motor/configs.rb +17 -0
  31. data/lib/motor/configs/build_configs_hash.rb +83 -0
  32. data/lib/motor/configs/build_ui_app_tag.rb +71 -0
  33. data/lib/motor/configs/load_from_cache.rb +81 -0
  34. data/lib/motor/configs/sync_from_file.rb +36 -0
  35. data/lib/motor/configs/sync_from_hash.rb +126 -0
  36. data/lib/motor/configs/sync_middleware.rb +72 -0
  37. data/lib/motor/configs/sync_with_remote.rb +47 -0
  38. data/lib/motor/configs/write_to_file.rb +36 -0
  39. data/lib/motor/dashboards/persistance.rb +15 -5
  40. data/lib/motor/forms/persistance.rb +15 -5
  41. data/lib/motor/net_http_utils.rb +38 -0
  42. data/lib/motor/queries/persistance.rb +13 -3
  43. data/lib/motor/railtie.rb +11 -0
  44. data/lib/motor/tasks/motor.rake +37 -0
  45. data/lib/motor/version.rb +1 -1
  46. data/ui/dist/{main-358ea31cd7020f915067.css.gz → main-05401628fabd32884fa6.css.gz} +0 -0
  47. data/ui/dist/main-05401628fabd32884fa6.js.gz +0 -0
  48. data/ui/dist/manifest.json +5 -5
  49. metadata +16 -6
  50. data/lib/motor/audited_utils.rb +0 -15
  51. data/lib/motor/ui_configs.rb +0 -82
  52. 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