motor-admin 0.1.37 → 0.1.43

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 (47) 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 +2 -2
  10. data/app/models/motor/dashboard.rb +1 -1
  11. data/app/models/motor/form.rb +1 -1
  12. data/app/models/motor/query.rb +2 -1
  13. data/app/views/motor/ui/show.html.erb +1 -1
  14. data/lib/generators/motor/templates/install.rb +10 -10
  15. data/lib/motor.rb +11 -3
  16. data/lib/motor/active_record_utils/defined_scopes_extension.rb +1 -1
  17. data/lib/motor/admin.rb +8 -0
  18. data/lib/motor/alerts/persistance.rb +17 -3
  19. data/lib/motor/alerts/scheduler.rb +1 -1
  20. data/lib/motor/api_query/apply_scope.rb +12 -5
  21. data/lib/motor/build_schema.rb +3 -3
  22. data/lib/motor/build_schema/find_display_column.rb +32 -29
  23. data/lib/motor/build_schema/load_from_rails.rb +32 -16
  24. data/lib/motor/build_schema/merge_schema_configs.rb +8 -4
  25. data/lib/motor/build_schema/reorder_schema.rb +10 -4
  26. data/lib/motor/configs.rb +17 -0
  27. data/lib/motor/configs/build_configs_hash.rb +83 -0
  28. data/lib/motor/configs/build_ui_app_tag.rb +71 -0
  29. data/lib/motor/configs/load_from_cache.rb +81 -0
  30. data/lib/motor/configs/sync_from_file.rb +36 -0
  31. data/lib/motor/configs/sync_from_hash.rb +126 -0
  32. data/lib/motor/configs/sync_middleware.rb +72 -0
  33. data/lib/motor/configs/sync_with_remote.rb +47 -0
  34. data/lib/motor/configs/write_to_file.rb +36 -0
  35. data/lib/motor/dashboards/persistance.rb +15 -5
  36. data/lib/motor/forms/persistance.rb +15 -5
  37. data/lib/motor/net_http_utils.rb +38 -0
  38. data/lib/motor/queries/persistance.rb +13 -3
  39. data/lib/motor/railtie.rb +11 -0
  40. data/lib/motor/tasks/motor.rake +37 -0
  41. data/lib/motor/version.rb +1 -1
  42. data/ui/dist/{main-358ea31cd7020f915067.css.gz → main-c3893a249308242d20f5.css.gz} +0 -0
  43. data/ui/dist/main-c3893a249308242d20f5.js.gz +0 -0
  44. data/ui/dist/manifest.json +5 -5
  45. metadata +16 -5
  46. data/lib/motor/ui_configs.rb +0 -82
  47. data/ui/dist/main-358ea31cd7020f915067.js.gz +0 -0
@@ -58,11 +58,11 @@ module Motor
58
58
 
59
59
  module_function
60
60
 
61
- def call
61
+ def call(cache_keys = {})
62
62
  schema = LoadFromRails.call
63
- schema = MergeSchemaConfigs.call(schema)
63
+ schema = MergeSchemaConfigs.call(schema, cache_keys)
64
64
 
65
- ReorderSchema.call(schema)
65
+ ReorderSchema.call(schema, cache_keys)
66
66
  end
67
67
  end
68
68
  end
@@ -3,35 +3,38 @@
3
3
  module Motor
4
4
  module BuildSchema
5
5
  module FindDisplayColumn
6
- DISPLAY_NAMES = %w[
7
- name
8
- full_name
9
- fullname
10
- last_name
11
- lastname
12
- first_name
13
- firstname
14
- fname
15
- lname
16
- sname
17
- phone
18
- phone_number
19
- email
20
- domain
21
- phone
22
- company
23
- filename
24
- file_name
25
- title
26
- url
27
- make
28
- brand
29
- manufacturer
30
- model
31
- address
32
- ].freeze
33
-
34
- DISPLAY_NAME_REGEXP = Regexp.new(Regexp.union(DISPLAY_NAMES).source, Regexp::IGNORECASE)
6
+ DISPLAY_NAMES = Set.new(
7
+ %w[
8
+ name
9
+ full_name
10
+ fullname
11
+ last_name
12
+ lastname
13
+ first_name
14
+ firstname
15
+ fname
16
+ lname
17
+ sname
18
+ company
19
+ domain
20
+ website
21
+ title
22
+ phone
23
+ phone_number
24
+ email
25
+ phone
26
+ filename
27
+ file_name
28
+ url
29
+ make
30
+ brand
31
+ manufacturer
32
+ model
33
+ address
34
+ ]
35
+ ).freeze
36
+
37
+ DISPLAY_NAME_REGEXP = Regexp.new(Regexp.union(DISPLAY_NAMES.to_a).source, Regexp::IGNORECASE)
35
38
 
36
39
  module_function
37
40
 
@@ -4,6 +4,7 @@ module Motor
4
4
  module BuildSchema
5
5
  module LoadFromRails
6
6
  MUTEX = Mutex.new
7
+ UNIFIED_TYPES = ActiveRecordUtils::Types::UNIFIED_TYPES
7
8
 
8
9
  module_function
9
10
 
@@ -20,8 +21,7 @@ module Motor
20
21
  def models
21
22
  eager_load_models!
22
23
 
23
- models = load_descendants(ActiveRecord::Base).uniq
24
- models = models.reject(&:abstract_class)
24
+ models = ActiveRecord::Base.descendants.reject(&:abstract_class)
25
25
 
26
26
  models -= Motor::ApplicationRecord.descendants
27
27
  models -= [Motor::Audit]
@@ -32,12 +32,6 @@ module Motor
32
32
  models
33
33
  end
34
34
 
35
- def load_descendants(model)
36
- model.descendants + model.descendants.flat_map do |klass|
37
- load_descendants(klass)
38
- end
39
- end
40
-
41
35
  def build_model_schema(model)
42
36
  model_name = model.name
43
37
 
@@ -92,10 +86,13 @@ module Motor
92
86
  end
93
87
 
94
88
  def build_table_column(column, model, default_attrs)
89
+ is_enum = model.defined_enums[column.name]
90
+
95
91
  {
96
92
  name: column.name,
97
93
  display_name: column.name.humanize,
98
- column_type: ActiveRecordUtils::Types::UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
94
+ column_type: is_enum ? 'string' : UNIFIED_TYPES[column.type.to_s] || column.type.to_s,
95
+ is_array: column.array?,
99
96
  access_type: COLUMN_NAME_ACCESS_TYPES.fetch(column.name, ColumnAccessTypes::READ_WRITE),
100
97
  default_value: default_attrs[column.name],
101
98
  validators: fetch_validators(model, column.name),
@@ -136,17 +133,24 @@ module Motor
136
133
  default_value: default_attrs[column_name],
137
134
  validators: fetch_validators(model, column_name, ref),
138
135
  format: {},
139
- reference: {
140
- name: name,
141
- model_name: ref.klass.name.underscore,
142
- reference_type: ref.belongs_to? ? 'belongs_to' : 'has_one',
143
- foreign_key: ref.foreign_key,
144
- polymorphic: ref.polymorphic? || is_attachment
145
- },
136
+ reference: build_reference(name, ref),
146
137
  virtual: false
147
138
  }
148
139
  end
149
140
 
141
+ def build_reference(name, reflection)
142
+ is_attachment = reflection.klass.name == 'ActiveStorage::Attachment'
143
+
144
+ {
145
+ name: name,
146
+ display_name: name.humanize,
147
+ model_name: reflection.klass.name.underscore,
148
+ reference_type: reflection.belongs_to? ? 'belongs_to' : 'has_one',
149
+ foreign_key: reflection.foreign_key,
150
+ polymorphic: reflection.polymorphic? || is_attachment
151
+ }
152
+ end
153
+
150
154
  def fetch_associations(model)
151
155
  model.reflections.map do |name, ref|
152
156
  next if ref.has_one? || ref.belongs_to?
@@ -181,6 +185,10 @@ module Motor
181
185
  []
182
186
  end
183
187
 
188
+ enum = model.defined_enums[column_name]
189
+
190
+ validators += [{ includes: enum.keys }] if enum
191
+
184
192
  validators += model.validators_on(column_name).map do |validator|
185
193
  build_validator_hash(validator)
186
194
  end.compact
@@ -210,6 +218,14 @@ module Motor
210
218
  else
211
219
  Rails.application.eager_load!
212
220
  end
221
+
222
+ ActiveRecord::Base.descendants.each do |model|
223
+ model.reflections.each do |_, ref|
224
+ ref.klass
225
+ rescue StandardError
226
+ next
227
+ end
228
+ end
213
229
  end
214
230
  end
215
231
  end
@@ -12,9 +12,10 @@ module Motor
12
12
  module_function
13
13
 
14
14
  # @param schema [Array<HashWithIndifferentAccess>]
15
+ # @param cache_keys [Hash]
15
16
  # @return [Array<HashWithIndifferentAccess>]
16
- def call(schema)
17
- configs = load_configs
17
+ def call(schema, cache_keys = {})
18
+ configs = load_configs(cache_keys)
18
19
 
19
20
  schema.map do |model|
20
21
  merge_model(model, configs.fetch(model[:name], {}))
@@ -120,9 +121,12 @@ module Motor
120
121
  end.compact
121
122
  end
122
123
 
124
+ # @param cache_keys [Hash]
123
125
  # @return [HashWithIndifferentAccess<String, HashWithIndifferentAccess>]
124
- def load_configs
125
- Motor::Resource.all.each_with_object(HashWithIndifferentAccess.new) do |resource, acc|
126
+ def load_configs(cache_keys)
127
+ resources = Motor::Configs::LoadFromCache.load_resources(cache_key: cache_keys[:resources])
128
+
129
+ resources.each_with_object(HashWithIndifferentAccess.new) do |resource, acc|
126
130
  acc[resource.name] = resource.preferences
127
131
  end
128
132
  end
@@ -16,10 +16,11 @@ module Motor
16
16
 
17
17
  module_function
18
18
 
19
+ # @param cache_keys [Hash]
19
20
  # @param schema [Array<HashWithIndifferentAccess>]
20
21
  # @return [Array<HashWithIndifferentAccess>]
21
- def call(schema)
22
- configs = load_configs
22
+ def call(schema, cache_keys = {})
23
+ configs = load_configs(cache_keys)
23
24
 
24
25
  schema = sort_by_name(schema, configs['resources.order'])
25
26
 
@@ -64,6 +65,8 @@ module Motor
64
65
  end
65
66
  end
66
67
 
68
+ # @param columns [Array<HashWithIndifferentAccess>]
69
+ # @return [Array<HashWithIndifferentAccess>]
67
70
  def sort_columns(columns)
68
71
  columns.each_with_object([]) do |column, acc|
69
72
  weight = COLUMNS_DEFAULT_ORDER_WEIGHTS.fetch(column[:name], COLUMNS_DEFAULT_ORDER_WEIGHT)
@@ -72,9 +75,12 @@ module Motor
72
75
  end.flatten.compact
73
76
  end
74
77
 
78
+ # @param cache_keys [Hash]
75
79
  # @return [Hash<String, HashWithIndifferentAccess>]
76
- def load_configs
77
- Motor::Config.all.each_with_object({}) do |config, acc|
80
+ def load_configs(cache_keys = {})
81
+ configs = Motor::Configs::LoadFromCache.load_configs(cache_key: cache_keys[:configs])
82
+
83
+ configs.each_with_object({}) do |config, acc|
78
84
  acc[config.key] = config.value
79
85
  end
80
86
  end
@@ -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