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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +32 -0
- data/Rakefile +20 -0
- data/app/controllers/motor/alerts_controller.rb +74 -0
- data/app/controllers/motor/api_base_controller.rb +25 -0
- data/app/controllers/motor/application_controller.rb +6 -0
- data/app/controllers/motor/assets_controller.rb +28 -0
- data/app/controllers/motor/configs_controller.rb +30 -0
- data/app/controllers/motor/dashboards_controller.rb +54 -0
- data/app/controllers/motor/data_controller.rb +102 -0
- data/app/controllers/motor/forms_controller.rb +54 -0
- data/app/controllers/motor/queries_controller.rb +54 -0
- data/app/controllers/motor/resource_methods_controller.rb +21 -0
- data/app/controllers/motor/resources_controller.rb +23 -0
- data/app/controllers/motor/run_queries_controller.rb +45 -0
- data/app/controllers/motor/schemas_controller.rb +11 -0
- data/app/controllers/motor/send_alerts_controller.rb +24 -0
- data/app/controllers/motor/tags_controller.rb +11 -0
- data/app/controllers/motor/ui_controller.rb +19 -0
- data/app/jobs/motor/alert_sending_job.rb +13 -0
- data/app/jobs/motor/application_job.rb +6 -0
- data/app/mailers/motor/alerts_mailer.rb +50 -0
- data/app/mailers/motor/application_mailer.rb +7 -0
- data/app/models/motor/alert.rb +22 -0
- data/app/models/motor/alert_lock.rb +7 -0
- data/app/models/motor/application_record.rb +8 -0
- data/app/models/motor/config.rb +7 -0
- data/app/models/motor/dashboard.rb +18 -0
- data/app/models/motor/form.rb +14 -0
- data/app/models/motor/query.rb +14 -0
- data/app/models/motor/resource.rb +7 -0
- data/app/models/motor/tag.rb +7 -0
- data/app/models/motor/taggable_tag.rb +8 -0
- data/app/views/layouts/motor/application.html.erb +14 -0
- data/app/views/motor/alerts_mailer/alert_email.html.erb +126 -0
- data/app/views/motor/ui/show.html.erb +1 -0
- data/config/routes.rb +60 -0
- data/lib/generators/motor/install_generator.rb +22 -0
- data/lib/generators/motor/migration.rb +17 -0
- data/lib/generators/motor/templates/install.rb +135 -0
- data/lib/motor-admin.rb +3 -0
- data/lib/motor.rb +47 -0
- data/lib/motor/active_record_utils.rb +7 -0
- data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
- data/lib/motor/active_record_utils/types.rb +54 -0
- data/lib/motor/admin.rb +12 -0
- data/lib/motor/alerts.rb +10 -0
- data/lib/motor/alerts/persistance.rb +84 -0
- data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
- data/lib/motor/alerts/scheduler.rb +30 -0
- data/lib/motor/api.rb +6 -0
- data/lib/motor/api_query.rb +22 -0
- data/lib/motor/api_query/build_json.rb +109 -0
- data/lib/motor/api_query/build_meta.rb +17 -0
- data/lib/motor/api_query/filter.rb +55 -0
- data/lib/motor/api_query/paginate.rb +18 -0
- data/lib/motor/api_query/search.rb +73 -0
- data/lib/motor/api_query/sort.rb +27 -0
- data/lib/motor/assets.rb +45 -0
- data/lib/motor/build_schema.rb +23 -0
- data/lib/motor/build_schema/find_display_column.rb +60 -0
- data/lib/motor/build_schema/load_from_rails.rb +176 -0
- data/lib/motor/build_schema/merge_schema_configs.rb +77 -0
- data/lib/motor/build_schema/persist_resource_configs.rb +208 -0
- data/lib/motor/build_schema/reorder_schema.rb +52 -0
- data/lib/motor/build_schema/utils.rb +17 -0
- data/lib/motor/dashboards.rb +8 -0
- data/lib/motor/dashboards/persistance.rb +63 -0
- data/lib/motor/forms.rb +8 -0
- data/lib/motor/forms/persistance.rb +63 -0
- data/lib/motor/hash_serializer.rb +21 -0
- data/lib/motor/queries.rb +10 -0
- data/lib/motor/queries/persistance.rb +63 -0
- data/lib/motor/queries/postgresql_exec_query.rb +28 -0
- data/lib/motor/queries/run_query.rb +68 -0
- data/lib/motor/tags.rb +31 -0
- data/lib/motor/ui_configs.rb +62 -0
- data/lib/motor/version.rb +5 -0
- data/ui/dist/fonts/ionicons.woff2 +0 -0
- data/ui/dist/main-46621a8bdbb789e17c3f.css.gz +0 -0
- data/ui/dist/main-46621a8bdbb789e17c3f.js.gz +0 -0
- data/ui/dist/manifest.json +13 -0
- 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
|
data/lib/motor/admin.rb
ADDED
@@ -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
|
data/lib/motor/alerts.rb
ADDED
@@ -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,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
|