motor-admin 0.1.9

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 (84) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +32 -0
  4. data/Rakefile +20 -0
  5. data/app/controllers/motor/alerts_controller.rb +74 -0
  6. data/app/controllers/motor/api_base_controller.rb +25 -0
  7. data/app/controllers/motor/application_controller.rb +6 -0
  8. data/app/controllers/motor/assets_controller.rb +28 -0
  9. data/app/controllers/motor/configs_controller.rb +30 -0
  10. data/app/controllers/motor/dashboards_controller.rb +54 -0
  11. data/app/controllers/motor/data_controller.rb +102 -0
  12. data/app/controllers/motor/forms_controller.rb +54 -0
  13. data/app/controllers/motor/queries_controller.rb +54 -0
  14. data/app/controllers/motor/resource_methods_controller.rb +21 -0
  15. data/app/controllers/motor/resources_controller.rb +23 -0
  16. data/app/controllers/motor/run_queries_controller.rb +45 -0
  17. data/app/controllers/motor/schemas_controller.rb +11 -0
  18. data/app/controllers/motor/send_alerts_controller.rb +24 -0
  19. data/app/controllers/motor/tags_controller.rb +11 -0
  20. data/app/controllers/motor/ui_controller.rb +19 -0
  21. data/app/jobs/motor/alert_sending_job.rb +13 -0
  22. data/app/jobs/motor/application_job.rb +6 -0
  23. data/app/mailers/motor/alerts_mailer.rb +50 -0
  24. data/app/mailers/motor/application_mailer.rb +7 -0
  25. data/app/models/motor/alert.rb +22 -0
  26. data/app/models/motor/alert_lock.rb +7 -0
  27. data/app/models/motor/application_record.rb +8 -0
  28. data/app/models/motor/config.rb +7 -0
  29. data/app/models/motor/dashboard.rb +18 -0
  30. data/app/models/motor/form.rb +14 -0
  31. data/app/models/motor/query.rb +14 -0
  32. data/app/models/motor/resource.rb +7 -0
  33. data/app/models/motor/tag.rb +7 -0
  34. data/app/models/motor/taggable_tag.rb +8 -0
  35. data/app/views/layouts/motor/application.html.erb +14 -0
  36. data/app/views/motor/alerts_mailer/alert_email.html.erb +126 -0
  37. data/app/views/motor/ui/show.html.erb +1 -0
  38. data/config/routes.rb +60 -0
  39. data/lib/generators/motor/install_generator.rb +22 -0
  40. data/lib/generators/motor/migration.rb +17 -0
  41. data/lib/generators/motor/templates/install.rb +135 -0
  42. data/lib/motor-admin.rb +3 -0
  43. data/lib/motor.rb +47 -0
  44. data/lib/motor/active_record_utils.rb +7 -0
  45. data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
  46. data/lib/motor/active_record_utils/types.rb +54 -0
  47. data/lib/motor/admin.rb +12 -0
  48. data/lib/motor/alerts.rb +10 -0
  49. data/lib/motor/alerts/persistance.rb +84 -0
  50. data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
  51. data/lib/motor/alerts/scheduler.rb +30 -0
  52. data/lib/motor/api.rb +6 -0
  53. data/lib/motor/api_query.rb +22 -0
  54. data/lib/motor/api_query/build_json.rb +109 -0
  55. data/lib/motor/api_query/build_meta.rb +17 -0
  56. data/lib/motor/api_query/filter.rb +55 -0
  57. data/lib/motor/api_query/paginate.rb +18 -0
  58. data/lib/motor/api_query/search.rb +73 -0
  59. data/lib/motor/api_query/sort.rb +27 -0
  60. data/lib/motor/assets.rb +45 -0
  61. data/lib/motor/build_schema.rb +23 -0
  62. data/lib/motor/build_schema/find_display_column.rb +60 -0
  63. data/lib/motor/build_schema/load_from_rails.rb +176 -0
  64. data/lib/motor/build_schema/merge_schema_configs.rb +77 -0
  65. data/lib/motor/build_schema/persist_resource_configs.rb +208 -0
  66. data/lib/motor/build_schema/reorder_schema.rb +52 -0
  67. data/lib/motor/build_schema/utils.rb +17 -0
  68. data/lib/motor/dashboards.rb +8 -0
  69. data/lib/motor/dashboards/persistance.rb +63 -0
  70. data/lib/motor/forms.rb +8 -0
  71. data/lib/motor/forms/persistance.rb +63 -0
  72. data/lib/motor/hash_serializer.rb +21 -0
  73. data/lib/motor/queries.rb +10 -0
  74. data/lib/motor/queries/persistance.rb +63 -0
  75. data/lib/motor/queries/postgresql_exec_query.rb +28 -0
  76. data/lib/motor/queries/run_query.rb +68 -0
  77. data/lib/motor/tags.rb +31 -0
  78. data/lib/motor/ui_configs.rb +62 -0
  79. data/lib/motor/version.rb +5 -0
  80. data/ui/dist/fonts/ionicons.woff2 +0 -0
  81. data/ui/dist/main-46621a8bdbb789e17c3f.css.gz +0 -0
  82. data/ui/dist/main-46621a8bdbb789e17c3f.js.gz +0 -0
  83. data/ui/dist/manifest.json +13 -0
  84. metadata +237 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ActiveRecordUtils
5
+ module Types
6
+ MUTEX = Mutex.new
7
+ DEFAULT_TYPE = 'string'
8
+
9
+ UNIFIED_TYPES = {
10
+ 'smallint' => 'integer',
11
+ 'int' => 'integer',
12
+ 'int8' => 'integer',
13
+ 'int16' => 'integer',
14
+ 'bigint' => 'integer',
15
+ 'numeric' => 'float',
16
+ 'decimal' => 'float',
17
+ 'text' => 'string',
18
+ 'citext' => 'string',
19
+ 'jsonb' => 'json',
20
+ 'timestamp' => 'datetime'
21
+ }.freeze
22
+
23
+ module_function
24
+
25
+ def all
26
+ @all || MUTEX.synchronize do
27
+ @all ||= build_types_hash
28
+ end
29
+ end
30
+
31
+ def find_class_for_name(name)
32
+ all.invert[name.to_s]
33
+ end
34
+
35
+ def find_name_for_class(klass)
36
+ name = all[klass.to_s]
37
+
38
+ return UNIFIED_TYPES.fetch(name, name) if name
39
+
40
+ DEFAULT_TYPE
41
+ end
42
+
43
+ def build_types_hash
44
+ type_map = ActiveRecord::Base.connection.send(:type_map)
45
+
46
+ type_map.instance_variable_get('@mapping').map do |name, type|
47
+ next unless name.is_a?(String)
48
+
49
+ [type.call.class.to_s, name]
50
+ end.compact.to_h
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Admin < ::Rails::Engine
5
+ initializer 'motor.alerts.scheduler' do
6
+ next if defined?(Sidekiq) && Sidekiq.server?
7
+
8
+ Motor::Alerts::Scheduler::SCHEDULER_TASK.execute
9
+ Motor::Alerts::ScheduledAlertsCache::UPDATE_ALERTS_TASK.execute
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './alerts/scheduler'
4
+ require_relative './alerts/scheduled_alerts_cache'
5
+ require_relative './alerts/persistance'
6
+
7
+ module Motor
8
+ module Alerts
9
+ end
10
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Alerts
5
+ module Persistance
6
+ ALERT_ATTRIBUTES = %i[
7
+ query_id
8
+ name
9
+ description
10
+ preferences
11
+ is_enabled
12
+ to_emails
13
+ ].freeze
14
+
15
+ NameAlreadyExists = Class.new(StandardError)
16
+ InvalidInterval = Class.new(StandardError)
17
+
18
+ NORMALIZE_INTERVAL_REGEXP = /\A(?:every\s+)?/i.freeze
19
+
20
+ module_function
21
+
22
+ def build_from_params(params, current_user = nil)
23
+ alert = assign_attributes(Alert.new, params)
24
+
25
+ alert.author = current_user
26
+
27
+ alert
28
+ end
29
+
30
+ def create_from_params!(params, current_user = nil)
31
+ raise NameAlreadyExists if Alert.exists?(['lower(name) = ?', params[:name].to_s.downcase])
32
+
33
+ alert = build_from_params(params, current_user)
34
+
35
+ raise InvalidInterval unless alert.cron
36
+
37
+ ApplicationRecord.transaction do
38
+ alert.save!
39
+ end
40
+
41
+ alert
42
+ rescue ActiveRecord::RecordNotUnique
43
+ retry
44
+ end
45
+
46
+ def update_from_params!(alert, params)
47
+ raise NameAlreadyExists if name_already_exists?(alert)
48
+
49
+ alert = assign_attributes(alert, params)
50
+
51
+ raise InvalidInterval unless alert.cron
52
+
53
+ ApplicationRecord.transaction do
54
+ alert.save!
55
+ end
56
+
57
+ alert.tags.reload
58
+
59
+ alert
60
+ rescue ActiveRecord::RecordNotUnique
61
+ retry
62
+ end
63
+
64
+ def assign_attributes(alert, params)
65
+ alert.assign_attributes(params.slice(*ALERT_ATTRIBUTES))
66
+ alert.preferences[:interval] = normalize_interval(alert.preferences[:interval])
67
+
68
+ Motor::Tags.assign_tags(alert, params[:tags])
69
+ end
70
+
71
+ def normalize_interval(interval)
72
+ interval.to_s.gsub(NORMALIZE_INTERVAL_REGEXP, 'every ')
73
+ end
74
+
75
+ def name_already_exists?(alert)
76
+ if alert.new_record?
77
+ Alert.exists?(['lower(name) = ?', alert.name.to_s.downcase])
78
+ else
79
+ Alert.exists?(['lower(name) = ? AND id != ?', alert.name.to_s.downcase, alert.id])
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Alerts
5
+ module ScheduledAlertsCache
6
+ UPDATE_ALERTS_TASK = Concurrent::TimerTask.new(
7
+ execution_interval: 2.minutes
8
+ ) { Motor::Alerts::ScheduledAlertsCache.load_alerts }
9
+
10
+ CACHE_STORE = ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
11
+
12
+ module_function
13
+
14
+ def all
15
+ ActiveRecord::Base.logger.silence do
16
+ CACHE_STORE.fetch(Motor::Alert.all.maximum(:updated_at)) do
17
+ clear
18
+
19
+ Motor::Alert.all.active.enabled.to_a
20
+ end
21
+ end
22
+ end
23
+
24
+ def clear
25
+ CACHE_STORE.clear
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Alerts
5
+ module Scheduler
6
+ SCHEDULER_INTERVAL = 10.seconds
7
+ CHECK_BEHIND_DURATION = 15.minutes
8
+
9
+ SCHEDULER_TASK = Concurrent::TimerTask.new(
10
+ execution_interval: SCHEDULER_INTERVAL
11
+ ) { Motor::Alerts::Scheduler.call }
12
+
13
+ ALREADY_PROCESSED_CACHE = ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
14
+
15
+ module_function
16
+
17
+ def call
18
+ ScheduledAlertsCache.all.each do |alert|
19
+ next unless (CHECK_BEHIND_DURATION.ago..Time.current).cover?(alert.cron.previous_time.to_local_time)
20
+
21
+ ALREADY_PROCESSED_CACHE.fetch("#{alert.id}-#{alert.cron.previous_time.to_i}") do
22
+ Motor::AlertSendingJob.perform_later(alert).job_id
23
+ end
24
+ rescue StandardError => e
25
+ Rials.logger.error(e)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/motor/api.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Api < ::Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './api_query/sort'
4
+ require_relative './api_query/paginate'
5
+ require_relative './api_query/filter'
6
+ require_relative './api_query/search'
7
+ require_relative './api_query/build_meta'
8
+ require_relative './api_query/build_json'
9
+
10
+ module Motor
11
+ module ApiQuery
12
+ module_function
13
+
14
+ def call(rel, params)
15
+ rel = ApiQuery::Sort.call(rel, params[:sort])
16
+ rel = ApiQuery::Paginate.call(rel, params[:page])
17
+ rel = ApiQuery::Filter.call(rel, params[:filter])
18
+
19
+ ApiQuery::Search.call(rel, params[:q] || params[:search] || params[:query])
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module BuildJson
6
+ module_function
7
+
8
+ def call(rel, params)
9
+ rel = rel.none if params.dig(:page, :limit).yield_self { |limit| limit.present? && limit.to_i.zero? }
10
+
11
+ rel = rel.preload_associations_lazily if rel.is_a?(ActiveRecord::Relation)
12
+
13
+ json_params = {}
14
+
15
+ assign_include_params(json_params, rel, params)
16
+ assign_fields_params(json_params, rel, params)
17
+
18
+ rel.as_json(json_params.with_indifferent_access)
19
+ end
20
+
21
+ def assign_include_params(json_params, _rel, api_params)
22
+ return if api_params['include'].blank?
23
+
24
+ include_params = api_params['include']
25
+
26
+ if include_params.is_a?(String)
27
+ include_params =
28
+ include_params.split(',').reduce({}) do |accumulator, path|
29
+ hash = {}
30
+
31
+ path.split('.').reduce(hash) do |acc, part|
32
+ acc[part] = {}
33
+
34
+ acc[part]
35
+ end
36
+
37
+ accumulator.deep_merge(hash)
38
+ end
39
+ end
40
+
41
+ json_params.deep_merge!(normalize_include_params(include_params))
42
+ end
43
+
44
+ def assign_fields_params(json_params, rel, params)
45
+ return if params[:fields].blank?
46
+
47
+ model = rel.is_a?(ActiveRecord::Relation) ? rel.klass : rel.class
48
+ model_name = model.name.underscore
49
+
50
+ params[:fields].each do |key, fields|
51
+ fields = fields.split(',') if fields.is_a?(String)
52
+ fields_hash = build_fields_hash(model, fields)
53
+
54
+ if key == model_name || model_name.split('/').last == key
55
+ json_params.merge!(fields_hash)
56
+ else
57
+ hash = find_key_in_params(json_params, key)
58
+
59
+ hash.merge!(fields_hash)
60
+ end
61
+ end
62
+ end
63
+
64
+ def build_fields_hash(model, fields)
65
+ columns = model.columns.map(&:name)
66
+ fields_hash = { 'only' => [], 'methods' => [] }
67
+
68
+ fields.each_with_object(fields_hash) do |field, acc|
69
+ if field.in?(columns)
70
+ acc['only'] << field
71
+ else
72
+ acc['methods'] << field
73
+ end
74
+ end
75
+ end
76
+
77
+ def find_key_in_params(params, key)
78
+ params = params['include']
79
+
80
+ return if params.blank?
81
+ return params[key] if params[key]
82
+
83
+ params.keys.reduce(nil) do |acc, k|
84
+ acc || find_key_in_params(params[k], key)
85
+ end
86
+ end
87
+
88
+ def normalize_include_params(params)
89
+ case params
90
+ when Array
91
+ params.each_with_object({}) do |name, hash|
92
+ hash[name] = { 'include' => {} }
93
+ end
94
+ when String
95
+ { params => { 'include' => {} } }
96
+ when Hash
97
+ include_hash =
98
+ params.transform_values do |value|
99
+ normalize_include_params(value)
100
+ end
101
+
102
+ { 'include' => include_hash }
103
+ else
104
+ raise ArgumentError, "Wrong include param type #{params.class}"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module BuildMeta
6
+ module_function
7
+
8
+ def call(rel, params)
9
+ meta = {}
10
+
11
+ meta[:count] = rel.limit(nil).offset(nil).reorder(nil).count if params[:meta].to_s.include?('count')
12
+
13
+ meta
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module Filter
6
+ LIKE_FILTER_VALUE_REGEXP = /\A%?(.*?)%?\z/.freeze
7
+
8
+ module_function
9
+
10
+ def call(rel, params)
11
+ return rel if params.blank?
12
+
13
+ normalized_params = normalize_params(Array.wrap(params))
14
+
15
+ rel.filter(normalized_params).distinct
16
+ end
17
+
18
+ def normalize_params(params)
19
+ params.map do |item|
20
+ next item if item.is_a?(String)
21
+ next normalize_params(item) if item.is_a?(Array)
22
+
23
+ item = item.to_unsafe_h if item.respond_to?(:to_unsafe_h)
24
+
25
+ item.transform_values do |filter|
26
+ if filter.is_a?(Hash)
27
+ normalize_filter_hash(filter)
28
+ else
29
+ filter
30
+ end
31
+ end
32
+ end.split('OR').product(['OR']).flatten(1)[0...-1]
33
+ end
34
+
35
+ def normalize_filter_hash(hash)
36
+ hash.each_with_object({}) do |(action, value), acc|
37
+ acc[action] =
38
+ if value.is_a?(Hash)
39
+ normalize_filter_hash(value)
40
+ else
41
+ normalize_action_value(action, value)
42
+ end
43
+ end
44
+ end
45
+
46
+ def normalize_action_value(action, value)
47
+ if %w[like ilike].include?(action)
48
+ value.sub(LIKE_FILTER_VALUE_REGEXP, '%\1%')
49
+ else
50
+ value
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end