motor-admin 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
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