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