motor-admin 0.1.35 → 0.1.41

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 (56) 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 +12 -3
  24. data/lib/motor/active_record_utils/defined_scopes_extension.rb +1 -1
  25. data/lib/motor/admin.rb +8 -0
  26. data/lib/motor/alerts/persistance.rb +17 -3
  27. data/lib/motor/alerts/scheduler.rb +1 -1
  28. data/lib/motor/api_query/apply_scope.rb +12 -5
  29. data/lib/motor/api_query/sort.rb +1 -1
  30. data/lib/motor/build_schema.rb +3 -3
  31. data/lib/motor/build_schema/find_display_column.rb +4 -4
  32. data/lib/motor/build_schema/load_from_rails.rb +20 -10
  33. data/lib/motor/build_schema/merge_schema_configs.rb +8 -4
  34. data/lib/motor/build_schema/reorder_schema.rb +10 -4
  35. data/lib/motor/configs.rb +17 -0
  36. data/lib/motor/configs/build_configs_hash.rb +83 -0
  37. data/lib/motor/configs/build_ui_app_tag.rb +71 -0
  38. data/lib/motor/configs/load_from_cache.rb +81 -0
  39. data/lib/motor/configs/sync_from_file.rb +36 -0
  40. data/lib/motor/configs/sync_from_hash.rb +126 -0
  41. data/lib/motor/configs/sync_middleware.rb +72 -0
  42. data/lib/motor/configs/sync_with_remote.rb +47 -0
  43. data/lib/motor/configs/write_to_file.rb +36 -0
  44. data/lib/motor/dashboards/persistance.rb +15 -5
  45. data/lib/motor/forms/persistance.rb +15 -5
  46. data/lib/motor/net_http_utils.rb +38 -0
  47. data/lib/motor/queries/persistance.rb +13 -3
  48. data/lib/motor/railtie.rb +11 -0
  49. data/lib/motor/tasks/motor.rake +37 -0
  50. data/lib/motor/version.rb +1 -1
  51. data/ui/dist/{main-03c3b1d3390877206e02.css.gz → main-0ef3be65da8d3b0dbabb.css.gz} +0 -0
  52. data/ui/dist/main-0ef3be65da8d3b0dbabb.js.gz +0 -0
  53. data/ui/dist/manifest.json +5 -5
  54. metadata +33 -5
  55. data/lib/motor/ui_configs.rb +0 -82
  56. data/ui/dist/main-03c3b1d3390877206e02.js.gz +0 -0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ FILE_PATH = 'config/motor.yml'
6
+ SYNC_API_PATH = '/motor_configs_sync'
7
+ SYNC_ACCESS_KEY = ENV.fetch('MOTOR_SYNC_API_KEY', '')
8
+ end
9
+ end
10
+
11
+ require_relative './configs/load_from_cache'
12
+ require_relative './configs/build_ui_app_tag'
13
+ require_relative './configs/build_configs_hash'
14
+ require_relative './configs/write_to_file'
15
+ require_relative './configs/sync_from_hash'
16
+ require_relative './configs/sync_from_file'
17
+ require_relative './configs/sync_with_remote'
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ module BuildConfigsHash
6
+ module_function
7
+
8
+ def call
9
+ cache_keys = LoadFromCache.load_cache_keys
10
+
11
+ normalize_hash(
12
+ engine_version: Motor::VERSION,
13
+ file_version: cache_keys.values.compact.max.to_time,
14
+ resources: build_resources_hash(cache_keys[:resources]),
15
+ configs: build_configs_hash(cache_keys[:configs]),
16
+ queries: build_queries_hash(cache_keys[:queries]),
17
+ dashboards: build_dashboards_hash(cache_keys[:dashboards]),
18
+ forms: build_forms_hash(cache_keys[:forms]),
19
+ alerts: build_alerts_hash(cache_keys[:alerts])
20
+ )
21
+ end
22
+
23
+ def build_queries_hash(cache_key = nil)
24
+ Motor::Configs::LoadFromCache.load_queries(cache_key: cache_key).sort_by(&:id).map do |query|
25
+ query.slice(%i[id name sql_body description preferences])
26
+ .merge(tags: query.tags.map(&:name), updated_at: query.updated_at.to_time)
27
+ end
28
+ end
29
+
30
+ def build_dashboards_hash(cache_key = nil)
31
+ Motor::Configs::LoadFromCache.load_dashboards(cache_key: cache_key).sort_by(&:id).map do |dashboard|
32
+ dashboard.slice(%i[id title description preferences])
33
+ .merge(tags: dashboard.tags.map(&:name), updated_at: dashboard.updated_at.to_time)
34
+ end
35
+ end
36
+
37
+ def build_alerts_hash(cache_key = nil)
38
+ Motor::Configs::LoadFromCache.load_alerts(cache_key: cache_key).sort_by(&:id).map do |alert|
39
+ alert.slice(%i[id name query_id to_emails is_enabled description preferences])
40
+ .merge(tags: alert.tags.map(&:name), updated_at: alert.updated_at.to_time)
41
+ end
42
+ end
43
+
44
+ def build_forms_hash(cache_key = nil)
45
+ Motor::Configs::LoadFromCache.load_forms(cache_key: cache_key).sort_by(&:id).map do |form|
46
+ form.slice(%i[id name http_method api_path description preferences])
47
+ .merge(tags: form.tags.map(&:name), updated_at: form.updated_at.to_time)
48
+ end
49
+ end
50
+
51
+ def build_configs_hash(cache_key = nil)
52
+ Motor::Configs::LoadFromCache.load_configs(cache_key: cache_key).sort_by(&:key).map do |config|
53
+ {
54
+ key: config.key,
55
+ value: config.value,
56
+ updated_at: config.updated_at.to_time
57
+ }
58
+ end
59
+ end
60
+
61
+ def build_resources_hash(cache_key = nil)
62
+ Motor::Configs::LoadFromCache.load_resources(cache_key: cache_key).sort_by(&:name).map do |resource|
63
+ {
64
+ name: resource.name,
65
+ preferences: resource.preferences,
66
+ updated_at: resource.updated_at.to_time
67
+ }
68
+ end
69
+ end
70
+
71
+ def normalize_hash(value)
72
+ case value
73
+ when Hash, HashWithIndifferentAccess
74
+ value.to_h.stringify_keys.transform_values { |v| normalize_hash(v) }
75
+ when Array
76
+ value.map { |e| normalize_hash(e) }
77
+ else
78
+ value
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Configs
5
+ module BuildUiAppTag
6
+ CACHE_STORE =
7
+ if Motor.development?
8
+ ActiveSupport::Cache::NullStore.new
9
+ else
10
+ ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
11
+ end
12
+
13
+ module_function
14
+
15
+ def call
16
+ cache_keys = LoadFromCache.load_cache_keys
17
+
18
+ CACHE_STORE.fetch(cache_keys.hash) do
19
+ CACHE_STORE.clear
20
+
21
+ Motor::ApplicationController.helpers.content_tag(
22
+ :div, '', id: 'app', data: build_data(cache_keys)
23
+ )
24
+ end
25
+ end
26
+
27
+ # @return [Hash]
28
+ def build_data(cache_keys = {})
29
+ {
30
+ base_path: Motor::Admin.routes.url_helpers.motor_path,
31
+ schema: Motor::BuildSchema.call(cache_keys),
32
+ header_links: header_links_data_hash(cache_keys[:configs]),
33
+ queries: queries_data_hash(cache_keys[:queries]),
34
+ dashboards: dashboards_data_hash(cache_keys[:dashboards]),
35
+ alerts: alerts_data_hash(cache_keys[:alerts]),
36
+ forms: forms_data_hash(cache_keys[:forms])
37
+ }
38
+ end
39
+
40
+ def header_links_data_hash(cache_key = nil)
41
+ configs = Motor::Configs::LoadFromCache.load_configs(cache_key: cache_key)
42
+
43
+ configs.find { |c| c.key == 'header.links' }&.value || []
44
+ end
45
+
46
+ def queries_data_hash(cache_key = nil)
47
+ Motor::Configs::LoadFromCache.load_queries(cache_key: cache_key)
48
+ .as_json(only: %i[id name updated_at],
49
+ include: { tags: { only: %i[id name] } })
50
+ end
51
+
52
+ def dashboards_data_hash(cache_key = nil)
53
+ Motor::Configs::LoadFromCache.load_dashboards(cache_key: cache_key)
54
+ .as_json(only: %i[id title updated_at],
55
+ include: { tags: { only: %i[id name] } })
56
+ end
57
+
58
+ def alerts_data_hash(cache_key = nil)
59
+ Motor::Configs::LoadFromCache.load_alerts(cache_key: cache_key)
60
+ .as_json(only: %i[id name is_enabled updated_at],
61
+ include: { tags: { only: %i[id name] } })
62
+ end
63
+
64
+ def forms_data_hash(cache_key = nil)
65
+ Motor::Configs::LoadFromCache.load_forms(cache_key: cache_key)
66
+ .as_json(only: %i[id name updated_at],
67
+ include: { tags: { only: %i[id name] } })
68
+ end
69
+ end
70
+ end
71
+ end
@@ -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,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