motor-admin 0.1.34 → 0.1.40

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +70 -0
  3. data/app/controllers/motor/alerts_controller.rb +12 -6
  4. data/app/controllers/motor/audits_controller.rb +16 -0
  5. data/app/controllers/motor/configs_controller.rb +1 -0
  6. data/app/controllers/motor/dashboards_controller.rb +4 -0
  7. data/app/controllers/motor/data_controller.rb +2 -57
  8. data/app/controllers/motor/forms_controller.rb +4 -0
  9. data/app/controllers/motor/queries_controller.rb +4 -0
  10. data/app/controllers/motor/resources_controller.rb +1 -0
  11. data/app/controllers/motor/ui_controller.rb +4 -0
  12. data/app/models/motor/alert.rb +4 -2
  13. data/app/models/motor/application_record.rb +10 -0
  14. data/app/models/motor/audit.rb +9 -0
  15. data/app/models/motor/config.rb +2 -0
  16. data/app/models/motor/dashboard.rb +3 -1
  17. data/app/models/motor/form.rb +3 -1
  18. data/app/models/motor/query.rb +4 -1
  19. data/app/models/motor/resource.rb +2 -0
  20. data/app/views/motor/ui/show.html.erb +1 -1
  21. data/config/routes.rb +1 -0
  22. data/lib/generators/motor/templates/install.rb +40 -16
  23. data/lib/motor.rb +11 -2
  24. data/lib/motor/admin.rb +8 -0
  25. data/lib/motor/alerts/persistance.rb +17 -3
  26. data/lib/motor/alerts/scheduler.rb +1 -1
  27. data/lib/motor/api_query/sort.rb +1 -1
  28. data/lib/motor/build_schema.rb +3 -3
  29. data/lib/motor/build_schema/load_from_rails.rb +2 -1
  30. data/lib/motor/build_schema/merge_schema_configs.rb +8 -4
  31. data/lib/motor/build_schema/reorder_schema.rb +10 -4
  32. data/lib/motor/configs.rb +17 -0
  33. data/lib/motor/configs/build_configs_hash.rb +83 -0
  34. data/lib/motor/configs/build_ui_app_tag.rb +71 -0
  35. data/lib/motor/configs/load_from_cache.rb +81 -0
  36. data/lib/motor/configs/sync_from_file.rb +36 -0
  37. data/lib/motor/configs/sync_from_hash.rb +124 -0
  38. data/lib/motor/configs/sync_middleware.rb +72 -0
  39. data/lib/motor/configs/sync_with_remote.rb +47 -0
  40. data/lib/motor/configs/write_to_file.rb +36 -0
  41. data/lib/motor/dashboards/persistance.rb +15 -5
  42. data/lib/motor/forms/persistance.rb +15 -5
  43. data/lib/motor/net_http_utils.rb +38 -0
  44. data/lib/motor/queries/persistance.rb +13 -3
  45. data/lib/motor/railtie.rb +11 -0
  46. data/lib/motor/tasks/motor.rake +37 -0
  47. data/lib/motor/version.rb +1 -1
  48. data/ui/dist/{main-052729fa924c6434623f.css.gz → main-57d82791202293600221.css.gz} +0 -0
  49. data/ui/dist/main-57d82791202293600221.js.gz +0 -0
  50. data/ui/dist/manifest.json +5 -5
  51. metadata +33 -5
  52. data/lib/motor/ui_configs.rb +0 -82
  53. data/ui/dist/main-052729fa924c6434623f.js.gz +0 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ module LoadFromCache
6
+ CACHE_STORE = ActiveSupport::Cache::MemoryStore.new(size: 10.megabytes)
7
+
8
+ module_function
9
+
10
+ def call
11
+ cache_keys = load_cache_keys
12
+
13
+ {
14
+ configs: load_configs(cache_key: cache_keys[:configs]),
15
+ resources: load_resources(cache_key: cache_keys[:resources]),
16
+ queries: load_queries(cache_key: cache_keys[:queries]),
17
+ dashboards: load_dashboards(cache_key: cache_keys[:dashboards]),
18
+ alerts: load_alerts(cache_key: cache_keys[:alerts]),
19
+ forms: load_forms(cache_key: cache_keys[:forms])
20
+ }
21
+ end
22
+
23
+ def load_configs(cache_key: nil)
24
+ maybe_fetch_from_cache('configs', cache_key) do
25
+ Motor::Config.all.load
26
+ end
27
+ end
28
+
29
+ def load_resources(cache_key: nil)
30
+ maybe_fetch_from_cache('resources', cache_key) do
31
+ Motor::Resource.all.load
32
+ end
33
+ end
34
+
35
+ def load_queries(cache_key: nil)
36
+ maybe_fetch_from_cache('queries', cache_key) do
37
+ Motor::Query.all.active.preload(:tags).load
38
+ end
39
+ end
40
+
41
+ def load_dashboards(cache_key: nil)
42
+ maybe_fetch_from_cache('dashboards', cache_key) do
43
+ Motor::Dashboard.all.active.preload(:tags).load
44
+ end
45
+ end
46
+
47
+ def load_alerts(cache_key: nil)
48
+ maybe_fetch_from_cache('alerts', cache_key) do
49
+ Motor::Alert.all.active.preload(:tags).load
50
+ end
51
+ end
52
+
53
+ def load_forms(cache_key: nil)
54
+ maybe_fetch_from_cache('forms', cache_key) do
55
+ Motor::Form.all.active.preload(:tags).load
56
+ end
57
+ end
58
+
59
+ def maybe_fetch_from_cache(type, cache_key, &block)
60
+ return block.call unless cache_key
61
+
62
+ CACHE_STORE.fetch(type + cache_key.to_s, &block)
63
+ end
64
+
65
+ def load_cache_keys
66
+ ActiveRecord::Base.connection.execute(
67
+ "(#{
68
+ [
69
+ Motor::Config.select("'configs', MAX(updated_at)").to_sql,
70
+ Motor::Resource.select("'resources', MAX(updated_at)").to_sql,
71
+ Motor::Dashboard.select("'dashboards', MAX(updated_at)").to_sql,
72
+ Motor::Alert.select("'alerts', MAX(updated_at)").to_sql,
73
+ Motor::Query.select("'queries', MAX(updated_at)").to_sql,
74
+ Motor::Form.select("'forms', MAX(updated_at)").to_sql
75
+ ].join(') UNION (')
76
+ })"
77
+ ).to_a.map(&:values).to_h.with_indifferent_access
78
+ end
79
+ end
80
+ end
81
+ end
@@ -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,124 @@
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
+ configs_hash = configs_hash.with_indifferent_access
10
+
11
+ Motor::ApplicationRecord.transaction do
12
+ sync_queries(configs_hash)
13
+ sync_alerts(configs_hash)
14
+ sync_dashboards(configs_hash)
15
+ sync_forms(configs_hash)
16
+ sync_configs(configs_hash)
17
+ sync_resources(configs_hash)
18
+ end
19
+ end
20
+
21
+ def sync_queries(configs_hash)
22
+ sync_taggable(
23
+ Motor::Configs::LoadFromCache.load_queries,
24
+ configs_hash[:queries],
25
+ configs_hash[:file_version],
26
+ Motor::Queries::Persistance.method(:update_from_params!)
27
+ )
28
+ end
29
+
30
+ def sync_alerts(configs_hash)
31
+ sync_taggable(
32
+ Motor::Configs::LoadFromCache.load_alerts,
33
+ configs_hash[:alerts],
34
+ configs_hash[:file_version],
35
+ Motor::Alerts::Persistance.method(:update_from_params!)
36
+ )
37
+ end
38
+
39
+ def sync_forms(configs_hash)
40
+ sync_taggable(
41
+ Motor::Configs::LoadFromCache.load_forms,
42
+ configs_hash[:forms],
43
+ configs_hash[:file_version],
44
+ Motor::Forms::Persistance.method(:update_from_params!)
45
+ )
46
+ end
47
+
48
+ def sync_dashboards(configs_hash)
49
+ sync_taggable(
50
+ Motor::Configs::LoadFromCache.load_dashboards,
51
+ configs_hash[:dashboards],
52
+ configs_hash[:file_version],
53
+ Motor::Dashboards::Persistance.method(:update_from_params!)
54
+ )
55
+ end
56
+
57
+ def sync_configs(configs_hash)
58
+ configs_index = Motor::Configs::LoadFromCache.load_configs.index_by(&:key)
59
+
60
+ configs_hash[:configs].each do |attrs|
61
+ record = configs_index[attrs[:key]] || Motor::Config.new
62
+
63
+ next if record.updated_at && attrs[:updated_at] <= record.updated_at
64
+
65
+ record.update!(attrs)
66
+ end
67
+ end
68
+
69
+ def sync_resources(configs_hash)
70
+ resources_index = Motor::Configs::LoadFromCache.load_resources.index_by(&:name)
71
+
72
+ configs_hash[:resources].each do |attrs|
73
+ record = resources_index[attrs[:name]] || Motor::Resource.new
74
+
75
+ next if record.updated_at && attrs[:updated_at] <= record.updated_at
76
+
77
+ record.update!(attrs)
78
+ end
79
+ end
80
+
81
+ def sync_taggable(records, config_items, configs_timestamp, update_proc)
82
+ processed_records, create_items = update_taggable_items(records, config_items, update_proc)
83
+
84
+ create_taggable_items(create_items, records.klass, update_proc)
85
+
86
+ archive_taggable_items(records - processed_records, configs_timestamp)
87
+
88
+ ActiveRecord::Base.connection.reset_pk_sequence!(records.klass.table_name)
89
+ end
90
+
91
+ def update_taggable_items(records, config_items, update_proc)
92
+ record_ids_hash = records.index_by(&:id)
93
+
94
+ config_items.each_with_object([[], []]) do |attrs, (processed_acc, create_acc)|
95
+ record = record_ids_hash[attrs[:id]]
96
+
97
+ next create_acc << attrs unless record
98
+
99
+ processed_acc << record if record
100
+
101
+ next if record.updated_at >= attrs[:updated_at]
102
+
103
+ update_proc.call(record, attrs, force_replace: true)
104
+ end
105
+ end
106
+
107
+ def create_taggable_items(create_items, records_class, update_proc)
108
+ create_items.each do |attrs|
109
+ record = records_class.find_or_initialize_by(id: attrs[:id]).tap { |e| e.deleted_at = nil }
110
+
111
+ update_proc.call(record, attrs, force_replace: true)
112
+ end
113
+ end
114
+
115
+ def archive_taggable_items(records_to_remove, configs_timestamp)
116
+ records_to_remove.each do |record|
117
+ next if record.updated_at > configs_timestamp
118
+
119
+ record.update!(deleted_at: Time.current) if record.deleted_at.blank?
120
+ end
121
+ end
122
+ end
123
+ end
124
+ 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