motor-admin-cstham8 0.4.35
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +230 -0
- data/Rakefile +11 -0
- data/app/channels/motor/application_cable/channel.rb +14 -0
- data/app/channels/motor/application_cable/connection.rb +27 -0
- data/app/channels/motor/notes_channel.rb +9 -0
- data/app/channels/motor/notifications_channel.rb +9 -0
- data/app/controllers/concerns/motor/current_ability.rb +21 -0
- data/app/controllers/concerns/motor/current_user_method.rb +18 -0
- data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +73 -0
- data/app/controllers/concerns/motor/wrap_io_params.rb +25 -0
- data/app/controllers/motor/active_storage_attachments_controller.rb +64 -0
- data/app/controllers/motor/alerts_controller.rb +82 -0
- data/app/controllers/motor/api_base_controller.rb +33 -0
- data/app/controllers/motor/api_configs_controller.rb +54 -0
- data/app/controllers/motor/application_controller.rb +8 -0
- data/app/controllers/motor/assets_controller.rb +43 -0
- data/app/controllers/motor/audits_controller.rb +16 -0
- data/app/controllers/motor/auth_tokens_controller.rb +36 -0
- data/app/controllers/motor/configs_controller.rb +33 -0
- data/app/controllers/motor/dashboards_controller.rb +64 -0
- data/app/controllers/motor/data_controller.rb +88 -0
- data/app/controllers/motor/forms_controller.rb +61 -0
- data/app/controllers/motor/icons_controller.rb +22 -0
- data/app/controllers/motor/note_tags_controller.rb +13 -0
- data/app/controllers/motor/notes_controller.rb +58 -0
- data/app/controllers/motor/notifications_controller.rb +33 -0
- data/app/controllers/motor/queries_controller.rb +64 -0
- data/app/controllers/motor/reminders_controller.rb +38 -0
- data/app/controllers/motor/resource_default_queries_controller.rb +23 -0
- data/app/controllers/motor/resource_methods_controller.rb +23 -0
- data/app/controllers/motor/resources_controller.rb +26 -0
- data/app/controllers/motor/run_api_requests_controller.rb +56 -0
- data/app/controllers/motor/run_graphql_requests_controller.rb +48 -0
- data/app/controllers/motor/run_queries_controller.rb +77 -0
- data/app/controllers/motor/schema_controller.rb +31 -0
- data/app/controllers/motor/send_alerts_controller.rb +26 -0
- data/app/controllers/motor/sessions_controller.rb +23 -0
- data/app/controllers/motor/slack_conversations_controller.rb +11 -0
- data/app/controllers/motor/tags_controller.rb +11 -0
- data/app/controllers/motor/ui_controller.rb +51 -0
- data/app/controllers/motor/users_for_autocomplete_controller.rb +23 -0
- data/app/jobs/motor/alert_sending_job.rb +13 -0
- data/app/jobs/motor/application_job.rb +6 -0
- data/app/jobs/motor/notify_note_mentions_job.rb +9 -0
- data/app/jobs/motor/notify_reminder_job.rb +9 -0
- data/app/mailers/motor/alerts_mailer.rb +39 -0
- data/app/mailers/motor/application_mailer.rb +33 -0
- data/app/mailers/motor/notifications_mailer.rb +33 -0
- data/app/models/motor/alert.rb +30 -0
- data/app/models/motor/alert_lock.rb +7 -0
- data/app/models/motor/api_config.rb +28 -0
- data/app/models/motor/application_record.rb +18 -0
- data/app/models/motor/audit.rb +13 -0
- data/app/models/motor/config.rb +13 -0
- data/app/models/motor/dashboard.rb +26 -0
- data/app/models/motor/form.rb +23 -0
- data/app/models/motor/note.rb +18 -0
- data/app/models/motor/note_tag.rb +7 -0
- data/app/models/motor/note_tag_tag.rb +8 -0
- data/app/models/motor/notification.rb +14 -0
- data/app/models/motor/query.rb +33 -0
- data/app/models/motor/reminder.rb +13 -0
- data/app/models/motor/resource.rb +15 -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 +17 -0
- data/app/views/layouts/motor/mailer.html.erb +72 -0
- data/app/views/motor/alerts_mailer/alert_email.html.erb +54 -0
- data/app/views/motor/notifications_mailer/notify_mention_email.html.erb +28 -0
- data/app/views/motor/notifications_mailer/notify_reminder_email.html.erb +28 -0
- data/app/views/motor/ui/show.html.erb +1 -0
- data/config/locales/el.yml +420 -0
- data/config/locales/en.yml +340 -0
- data/config/locales/es.yml +420 -0
- data/config/locales/ja.yml +340 -0
- data/config/locales/pt.yml +416 -0
- data/config/routes.rb +65 -0
- data/lib/generators/motor/install_generator.rb +24 -0
- data/lib/generators/motor/install_notes_generator.rb +22 -0
- data/lib/generators/motor/migration.rb +17 -0
- data/lib/generators/motor/templates/install.rb +271 -0
- data/lib/generators/motor/templates/install_api_configs.rb +86 -0
- data/lib/generators/motor/templates/install_notes.rb +83 -0
- data/lib/generators/motor/templates/upgrade_motor_api_actions.rb +71 -0
- data/lib/generators/motor/upgrade_generator.rb +43 -0
- data/lib/motor/active_record_utils/action_text_attribute_patch.rb +19 -0
- data/lib/motor/active_record_utils/active_record_connection_column_patch.rb +14 -0
- data/lib/motor/active_record_utils/active_record_filter.rb +405 -0
- data/lib/motor/active_record_utils/active_storage_blob_patch.rb +30 -0
- data/lib/motor/active_record_utils/active_storage_links_extension.rb +11 -0
- data/lib/motor/active_record_utils/defined_scopes_extension.rb +25 -0
- data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
- data/lib/motor/active_record_utils/types.rb +64 -0
- data/lib/motor/active_record_utils.rb +45 -0
- data/lib/motor/admin.rb +141 -0
- data/lib/motor/alerts/persistance.rb +97 -0
- data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
- data/lib/motor/alerts/scheduler.rb +30 -0
- data/lib/motor/alerts/slack_sender.rb +74 -0
- data/lib/motor/alerts.rb +52 -0
- data/lib/motor/api_configs.rb +41 -0
- data/lib/motor/api_query/apply_scope.rb +44 -0
- data/lib/motor/api_query/build_json.rb +171 -0
- data/lib/motor/api_query/build_meta.rb +20 -0
- data/lib/motor/api_query/filter.rb +125 -0
- data/lib/motor/api_query/paginate.rb +19 -0
- data/lib/motor/api_query/search.rb +60 -0
- data/lib/motor/api_query/sort.rb +64 -0
- data/lib/motor/api_query.rb +24 -0
- data/lib/motor/assets.rb +62 -0
- data/lib/motor/build_schema/active_storage_attachment_schema.rb +125 -0
- data/lib/motor/build_schema/adjust_devise_model_schema.rb +60 -0
- data/lib/motor/build_schema/apply_permissions.rb +64 -0
- data/lib/motor/build_schema/defaults.rb +66 -0
- data/lib/motor/build_schema/find_display_column.rb +65 -0
- data/lib/motor/build_schema/find_icon.rb +135 -0
- data/lib/motor/build_schema/find_searchable_columns.rb +33 -0
- data/lib/motor/build_schema/load_from_rails.rb +361 -0
- data/lib/motor/build_schema/merge_schema_configs.rb +157 -0
- data/lib/motor/build_schema/reorder_schema.rb +88 -0
- data/lib/motor/build_schema/utils.rb +31 -0
- data/lib/motor/build_schema.rb +125 -0
- data/lib/motor/cancan_utils/ability_patch.rb +31 -0
- data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
- data/lib/motor/cancan_utils.rb +9 -0
- data/lib/motor/configs/build_configs_hash.rb +90 -0
- data/lib/motor/configs/build_ui_app_tag.rb +177 -0
- data/lib/motor/configs/load_from_cache.rb +110 -0
- data/lib/motor/configs/sync_from_file.rb +35 -0
- data/lib/motor/configs/sync_from_hash.rb +159 -0
- data/lib/motor/configs/sync_middleware.rb +72 -0
- data/lib/motor/configs/sync_with_remote.rb +47 -0
- data/lib/motor/configs/write_to_file.rb +36 -0
- data/lib/motor/configs.rb +39 -0
- data/lib/motor/dashboards/persistance.rb +73 -0
- data/lib/motor/dashboards.rb +8 -0
- data/lib/motor/forms/persistance.rb +93 -0
- data/lib/motor/forms.rb +8 -0
- data/lib/motor/hash_serializer.rb +21 -0
- data/lib/motor/net_http_utils.rb +50 -0
- data/lib/motor/notes/notify_mentions.rb +71 -0
- data/lib/motor/notes/notify_reminder.rb +48 -0
- data/lib/motor/notes/persist.rb +36 -0
- data/lib/motor/notes/reminders_scheduler.rb +39 -0
- data/lib/motor/notes/tags.rb +34 -0
- data/lib/motor/notes.rb +12 -0
- data/lib/motor/queries/persistance.rb +90 -0
- data/lib/motor/queries/postgresql_exec_query.rb +28 -0
- data/lib/motor/queries/render_sql_template.rb +61 -0
- data/lib/motor/queries/run_query.rb +289 -0
- data/lib/motor/queries.rb +11 -0
- data/lib/motor/railtie.rb +11 -0
- data/lib/motor/resources/custom_sql_columns_cache.rb +17 -0
- data/lib/motor/resources/fetch_configured_model.rb +269 -0
- data/lib/motor/resources/persist_configs.rb +232 -0
- data/lib/motor/resources.rb +19 -0
- data/lib/motor/slack/client.rb +62 -0
- data/lib/motor/slack.rb +16 -0
- data/lib/motor/tags.rb +32 -0
- data/lib/motor/tasks/motor.rake +54 -0
- data/lib/motor/version.rb +5 -0
- data/lib/motor-admin-cstham8.rb +3 -0
- data/lib/motor.rb +87 -0
- data/ui/dist/manifest.json +1990 -0
- metadata +303 -0
data/lib/motor/admin.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
class Admin < ::Rails::Engine
|
5
|
+
config.custom_html = ''
|
6
|
+
|
7
|
+
ActiveSupport.cache_format_version = Rails.version.to_f
|
8
|
+
|
9
|
+
if !Motor.development? && Rails.env.development?
|
10
|
+
config.eager_load_paths.delete(File.expand_path('../../app/controllers', __dir__))
|
11
|
+
config.eager_load_paths.delete(File.expand_path('../../app/controllers/concerns', __dir__))
|
12
|
+
config.eager_load_paths.delete(File.expand_path('../../app/models', __dir__))
|
13
|
+
|
14
|
+
config.autoload_once_paths << File.expand_path('../../app/models', __dir__)
|
15
|
+
config.autoload_once_paths << File.expand_path('../../app/controllers', __dir__)
|
16
|
+
config.autoload_once_paths << File.expand_path('../../app/controllers/concerns', __dir__)
|
17
|
+
end
|
18
|
+
|
19
|
+
initializer 'motor.startup_message' do
|
20
|
+
config.after_initialize do
|
21
|
+
next unless Motor.server?
|
22
|
+
|
23
|
+
Rails.application.reload_routes!
|
24
|
+
|
25
|
+
if Rails.application.routes.url_helpers.respond_to?(:motor_admin_path)
|
26
|
+
url =
|
27
|
+
begin
|
28
|
+
Rails.application.routes.url_helpers.motor_admin_url
|
29
|
+
rescue ArgumentError
|
30
|
+
Rails.application.routes.url_helpers.motor_admin_path
|
31
|
+
end
|
32
|
+
|
33
|
+
puts
|
34
|
+
puts "⚡ Motor::Admin is starting under #{url}"
|
35
|
+
else
|
36
|
+
puts
|
37
|
+
puts '⚠️ Motor::Admin is not mounted.'
|
38
|
+
puts 'Add the following line to your config/routes.rb:'
|
39
|
+
puts
|
40
|
+
puts " mount Motor::Admin => '/admin'"
|
41
|
+
end
|
42
|
+
|
43
|
+
puts
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
initializer 'motor.configs.sync_middleware' do
|
48
|
+
next if Motor::Configs::SYNC_ACCESS_KEY.blank?
|
49
|
+
|
50
|
+
require 'motor/configs/sync_middleware'
|
51
|
+
|
52
|
+
Rails.application.config.middleware.insert_after(Rails::Rack::Logger, Motor::Configs::SyncMiddleware)
|
53
|
+
end
|
54
|
+
|
55
|
+
initializer 'motor.filter_params' do
|
56
|
+
Rails.application.config.filter_parameters += [/\Aio\z/]
|
57
|
+
end
|
58
|
+
|
59
|
+
initializer 'motor.alerts.scheduler' do
|
60
|
+
config.after_initialize do
|
61
|
+
next unless Motor.server?
|
62
|
+
next unless Rails.env.production?
|
63
|
+
|
64
|
+
Motor::Alerts::Scheduler::SCHEDULER_TASK.execute
|
65
|
+
Motor::Notes::RemindersScheduler::SCHEDULER_TASK.execute
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
initializer 'motor.basic_auth' do
|
70
|
+
next if ENV['MOTOR_AUTH_PASSWORD'].blank?
|
71
|
+
|
72
|
+
config.middleware.use Rack::Auth::Basic do |username, password|
|
73
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
74
|
+
::Digest::SHA256.hexdigest(username),
|
75
|
+
::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_USERNAME'].to_s)
|
76
|
+
) &
|
77
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
78
|
+
::Digest::SHA256.hexdigest(password),
|
79
|
+
::Digest::SHA256.hexdigest(ENV['MOTOR_AUTH_PASSWORD'].to_s)
|
80
|
+
)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
initializer 'warden.configure.dispatch_requests' do
|
85
|
+
next unless defined?(Warden::JWTAuth)
|
86
|
+
|
87
|
+
config.after_initialize do
|
88
|
+
Warden::JWTAuth.configure do |config|
|
89
|
+
config.dispatch_requests += [
|
90
|
+
['POST', /\A#{Regexp.escape(Motor::Admin.routes.url_helpers.motor_api_auth_tokens_path)}\z/]
|
91
|
+
]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
initializer 'action_cable.connection_class' do
|
97
|
+
config.after_initialize do
|
98
|
+
next if defined?(::ApplicationCable::Connection)
|
99
|
+
next unless defined?(::ActionCable)
|
100
|
+
|
101
|
+
ActionCable.server.config.connection_class = -> { Motor::ApplicationCable::Connection }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
initializer 'motor.active_storage.extensions' do
|
106
|
+
config.after_initialize do
|
107
|
+
next unless defined?(ActiveStorage::Engine)
|
108
|
+
|
109
|
+
ActiveSupport.on_load(:active_storage_attachment) do
|
110
|
+
ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
|
111
|
+
end
|
112
|
+
|
113
|
+
ActiveSupport.on_load(:active_storage_blob) do
|
114
|
+
ActiveStorage::Blob.singleton_class.prepend(Motor::ActiveRecordUtils::ActiveStorageBlobPatch)
|
115
|
+
end
|
116
|
+
|
117
|
+
ActiveStorage::Attachment.include(Motor::ActiveRecordUtils::ActiveStorageLinksExtension)
|
118
|
+
ActiveStorage::Blob.singleton_class.prepend(Motor::ActiveRecordUtils::ActiveStorageBlobPatch)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
initializer 'motor.upgrade' do
|
123
|
+
ActiveSupport.on_load(:motor_query) do
|
124
|
+
next unless Motor.server?
|
125
|
+
|
126
|
+
unless Motor::Query.table_exists?
|
127
|
+
puts
|
128
|
+
puts ' => Run `rails g motor:install && rake db:migrate` in order to create Motor Admin configuration tables'
|
129
|
+
puts
|
130
|
+
end
|
131
|
+
|
132
|
+
if !Motor::ApiConfig.table_exists? || !Motor::Note.table_exists?
|
133
|
+
puts
|
134
|
+
puts ' => Run `rails g motor:upgrade && rake db:migrate` ' \
|
135
|
+
'to perform data migration and enable the latest features'
|
136
|
+
puts
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,97 @@
|
|
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?(name: params[:name])
|
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, force_replace: false)
|
47
|
+
tag_ids = alert.tags.ids
|
48
|
+
|
49
|
+
alert = assign_attributes(alert, params)
|
50
|
+
|
51
|
+
raise NameAlreadyExists if !force_replace && name_already_exists?(alert)
|
52
|
+
raise InvalidInterval unless alert.cron
|
53
|
+
|
54
|
+
ApplicationRecord.transaction do
|
55
|
+
archive_with_existing_name(alert) if force_replace
|
56
|
+
|
57
|
+
alert.save!
|
58
|
+
end
|
59
|
+
|
60
|
+
alert.touch if tags_changed?(tag_ids, alert) && params[:updated_at].blank?
|
61
|
+
|
62
|
+
alert
|
63
|
+
rescue ActiveRecord::RecordNotUnique
|
64
|
+
retry
|
65
|
+
end
|
66
|
+
|
67
|
+
def tags_changed?(previous_ids, alert)
|
68
|
+
previous_ids.sort != alert.tags.reload.ids.sort
|
69
|
+
end
|
70
|
+
|
71
|
+
def assign_attributes(alert, params)
|
72
|
+
alert.assign_attributes(params.slice(*ALERT_ATTRIBUTES))
|
73
|
+
alert.preferences[:interval] = normalize_interval(alert.preferences[:interval])
|
74
|
+
alert.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
|
75
|
+
|
76
|
+
Motor::Tags.assign_tags(alert, params[:tags])
|
77
|
+
end
|
78
|
+
|
79
|
+
def archive_with_existing_name(alert)
|
80
|
+
Motor::Alert.where(['name = ? AND id != ?', alert.name, alert.id])
|
81
|
+
.update_all(deleted_at: Time.current)
|
82
|
+
end
|
83
|
+
|
84
|
+
def normalize_interval(interval)
|
85
|
+
interval.to_s.gsub(NORMALIZE_INTERVAL_REGEXP, 'every ')
|
86
|
+
end
|
87
|
+
|
88
|
+
def name_already_exists?(alert)
|
89
|
+
if alert.new_record?
|
90
|
+
Alert.exists?(name: alert.name, deleted_at: nil)
|
91
|
+
else
|
92
|
+
Alert.exists?(['name = ? AND id != ? AND deleted_at IS NULL', alert.name, alert.id])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Alerts
|
5
|
+
module ScheduledAlertsCache
|
6
|
+
CACHE_STORE = ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def all
|
11
|
+
ActiveRecord::Base.logger.silence do
|
12
|
+
cache_key = Motor::Alert.maximum(:updated_at)
|
13
|
+
|
14
|
+
return [] if cache_key.nil?
|
15
|
+
|
16
|
+
CACHE_STORE.fetch(cache_key) 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 = 1.minute
|
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.find_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
|
+
Rails.logger.error(e)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Alerts
|
5
|
+
module SlackSender
|
6
|
+
MARKDOWN_LINK_TRANSFORM_REGEXP = /\[(.+?)\]\((.+?)\)/.freeze
|
7
|
+
MARKDOWN_PATHS_TRANSFORM_REGEXP = /\n\s*\#{1,5}\s*(.*)\n?/.freeze
|
8
|
+
MARKDOWN_HEADING_TRANSFORM_REGEXP = /\n\s*\#{1,5}\s*(.*)\n?/.freeze
|
9
|
+
MARKDOWN_BOLD_TRANSFORM_REGEXP = /[*_]{2}/.freeze
|
10
|
+
|
11
|
+
DEFAULT_FILENAME = 'data.csv'
|
12
|
+
|
13
|
+
module_function
|
14
|
+
|
15
|
+
def call(alert, conversation_id, slack_user: nil)
|
16
|
+
query_result = load_data(alert, slack_user)
|
17
|
+
|
18
|
+
if alert.query.preferences[:visualization] == 'markdown'
|
19
|
+
text = [build_query_link(alert), render_markdown(alert, query_result)].join
|
20
|
+
|
21
|
+
Slack::Client.send_message(channel: conversation_id,
|
22
|
+
as_user: slack_user.present?,
|
23
|
+
text: text)
|
24
|
+
else
|
25
|
+
Slack::Client.send_message(channel: conversation_id,
|
26
|
+
as_user: slack_user.present?,
|
27
|
+
text: build_alert_message(alert))
|
28
|
+
Slack::Client.send_file(channels: conversation_id,
|
29
|
+
content: generate_csv(query_result),
|
30
|
+
filename: DEFAULT_FILENAME)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_alert_message(alert)
|
35
|
+
<<~MARKDOWN
|
36
|
+
#{build_query_link(alert)}
|
37
|
+
#{alert.description}
|
38
|
+
MARKDOWN
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_query_link(alert)
|
42
|
+
link = Motor::Admin.routes.url_helpers.motor_ui_query_url(alert.query_id, host: Motor.app_host)
|
43
|
+
|
44
|
+
"*<#{link}|#{alert.name}>*"
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_markdown(alert, query_result)
|
48
|
+
params = query_result.columns.pluck(:name).zip(query_result.data[0]).to_h.symbolize_keys
|
49
|
+
|
50
|
+
markdown =
|
51
|
+
["\n", Mustache.render(alert.query.preferences[:visualization_options][:markdown], **params)].join
|
52
|
+
|
53
|
+
markdown.gsub(/\((.*)\)/, "(http://#{Motor.app_host}\\1)")
|
54
|
+
.gsub(MARKDOWN_LINK_TRANSFORM_REGEXP, '<\2|\1>')
|
55
|
+
.gsub(MARKDOWN_HEADING_TRANSFORM_REGEXP, "\n\n*\\1*")
|
56
|
+
.gsub(MARKDOWN_BOLD_TRANSFORM_REGEXP, '*')
|
57
|
+
end
|
58
|
+
|
59
|
+
def generate_csv(query_result)
|
60
|
+
rows = [query_result.columns.pluck(:name)] + query_result.data
|
61
|
+
|
62
|
+
rows.map(&:to_csv).join
|
63
|
+
end
|
64
|
+
|
65
|
+
def load_data(alert, slack_user = nil)
|
66
|
+
if slack_user
|
67
|
+
Queries::RunQuery.call(alert.query, variables_hash: { current_user_email: slack_user['email'] })
|
68
|
+
else
|
69
|
+
Queries::RunQuery.call(alert.query)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/motor/alerts.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Alerts
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def send_alert(alert)
|
8
|
+
if alert.preferences[:adapter] == 'slack'
|
9
|
+
send_slack_alert(alert)
|
10
|
+
else
|
11
|
+
send_email_alert(alert)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def send_email_alert(alert)
|
16
|
+
alert.to_emails.split(',').each do |email|
|
17
|
+
if email.include?('@')
|
18
|
+
Motor::AlertsMailer.alert_email(alert, email: email).deliver_now!
|
19
|
+
else
|
20
|
+
send_query_email_alerts(alert, email.gsub(/[{}]/, ''))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def send_query_email_alerts(alert, column_name)
|
26
|
+
result = Queries::RunQuery.call(alert.query)
|
27
|
+
|
28
|
+
column_index = result.columns.find_index { |c| c[:name] == column_name }
|
29
|
+
|
30
|
+
emails = result.data.pluck(column_index).uniq
|
31
|
+
|
32
|
+
emails.each do |email|
|
33
|
+
Motor::AlertsMailer.alert_email(alert, email: email).deliver_now!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def send_slack_alert(alert)
|
38
|
+
slack_users = Slack::Client.load_users(limit: Slack::ITEMS_LIMIT)['members']
|
39
|
+
|
40
|
+
alert.preferences[:slack_conversation_ids].each do |conversation_id|
|
41
|
+
user = slack_users.find { |u| u['id'] == conversation_id }
|
42
|
+
|
43
|
+
Motor::Alerts::SlackSender.call(alert, conversation_id, slack_user: user)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
require_relative 'alerts/scheduler'
|
50
|
+
require_relative 'alerts/scheduled_alerts_cache'
|
51
|
+
require_relative 'alerts/persistance'
|
52
|
+
require_relative 'alerts/slack_sender'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module ApiConfigs
|
5
|
+
METHODS = %w[get post put delete].freeze
|
6
|
+
|
7
|
+
DEFAULT_HEADERS = { 'Content-Type' => 'application/json' }.freeze
|
8
|
+
|
9
|
+
InvalidHttpMethod = Class.new(StandardError)
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def run(api_config, method: nil, path: nil, body: nil, params: {}, headers: {})
|
14
|
+
method ||= 'get'
|
15
|
+
|
16
|
+
raise InvalidHttpMethod unless METHODS.include?(method.downcase)
|
17
|
+
|
18
|
+
Motor::NetHttpUtils.public_send(
|
19
|
+
method.downcase.to_sym,
|
20
|
+
api_config.url.to_s.sub(%r{/?\z}, '/') + path.delete_prefix('/'),
|
21
|
+
params,
|
22
|
+
DEFAULT_HEADERS.merge(headers).merge(api_config.headers),
|
23
|
+
body&.to_json
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def run_grapql(api_config, query:, variables: {}, headers: {})
|
28
|
+
body = {
|
29
|
+
query: query,
|
30
|
+
variables: variables.merge(form_data: variables)
|
31
|
+
}
|
32
|
+
|
33
|
+
Motor::NetHttpUtils.post(
|
34
|
+
api_config.url,
|
35
|
+
{},
|
36
|
+
DEFAULT_HEADERS.merge(headers).merge(api_config.headers),
|
37
|
+
body.to_json
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module ApiQuery
|
5
|
+
module ApplyScope
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(rel, scope)
|
9
|
+
return rel if scope.blank?
|
10
|
+
|
11
|
+
scope_symbol = scope.to_sym
|
12
|
+
|
13
|
+
if rel.klass.defined_scopes.include?(scope_symbol)
|
14
|
+
rel.public_send(scope_symbol)
|
15
|
+
else
|
16
|
+
apply_filter_scope(rel, scope)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def apply_filter_scope(rel, scope)
|
21
|
+
configs = Motor::Resource.find_by(name: rel.klass.name.underscore)
|
22
|
+
|
23
|
+
return rel unless configs
|
24
|
+
|
25
|
+
scope_configs = configs.preferences[:scopes].find { |s| s[:name] == scope }
|
26
|
+
|
27
|
+
return rel unless scope_configs
|
28
|
+
return rel unless scope_configs[:preferences]
|
29
|
+
|
30
|
+
rel = ApiQuery::Filter.call(rel, scope_configs[:preferences][:filter])
|
31
|
+
|
32
|
+
apply_order(rel, scope_configs[:preferences][:sort])
|
33
|
+
end
|
34
|
+
|
35
|
+
def apply_order(rel, params)
|
36
|
+
return rel if params.blank?
|
37
|
+
|
38
|
+
sort_key, sort_order = params.values_at(:key, :order)
|
39
|
+
|
40
|
+
rel.order(sort_key => sort_order)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module ApiQuery
|
5
|
+
module BuildJson
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# @param rel [ActiveRecord::Base, ActiveRecord::Relation]
|
9
|
+
# @param params [Hash]
|
10
|
+
# @param current_ability [CanCan::Ability]
|
11
|
+
# @return [Hash]
|
12
|
+
def call(rel, params, current_ability = Motor::CancanUtils::CanManageAll.new)
|
13
|
+
rel = rel.none if limit_zero_params?(params)
|
14
|
+
rel = rel.preload_associations_lazily if rel.is_a?(ActiveRecord::Relation)
|
15
|
+
|
16
|
+
model = rel.is_a?(ActiveRecord::Relation) ? rel.klass : rel.class
|
17
|
+
|
18
|
+
include_hash = build_include_hash(params['include'])
|
19
|
+
models_index = build_models_index(model, include_hash)
|
20
|
+
|
21
|
+
json_params = normalize_include_params(include_hash)
|
22
|
+
|
23
|
+
assign_fields_params!(json_params, model, params, current_ability, models_index)
|
24
|
+
|
25
|
+
rel.as_json(json_params.with_indifferent_access)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param include_params [Hash]
|
29
|
+
# @return [Hash]
|
30
|
+
def build_include_hash(include_params)
|
31
|
+
return {} if include_params.blank?
|
32
|
+
|
33
|
+
if include_params.is_a?(String)
|
34
|
+
build_hash_from_string_path(include_params)
|
35
|
+
else
|
36
|
+
include_params
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param json_params [Hash]
|
41
|
+
# @param model [Class<ActiveRecord::Base>]
|
42
|
+
# @param params [Hash]
|
43
|
+
# @param current_ability [CanCan::Ability]
|
44
|
+
# @param models_index [Hash]
|
45
|
+
# @return [void]
|
46
|
+
def assign_fields_params!(json_params, model, params, current_ability, models_index)
|
47
|
+
return if params[:fields].blank?
|
48
|
+
|
49
|
+
params[:fields].each do |key, fields|
|
50
|
+
fields_model = models_index[key]
|
51
|
+
|
52
|
+
next unless model
|
53
|
+
|
54
|
+
fields = fields.split(',') if fields.is_a?(String)
|
55
|
+
|
56
|
+
fields_hash = fields_model == model ? json_params : find_key_in_params(json_params, key)
|
57
|
+
|
58
|
+
fields_hash.merge!(build_fields_hash(fields_model, fields, current_ability))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param model [Class<ActiveRecord::Base>]
|
63
|
+
# @param fields [Hash]
|
64
|
+
# @param current_ability [CanCan::Ability]
|
65
|
+
# @return [Hash]
|
66
|
+
def build_fields_hash(model, fields, current_ability)
|
67
|
+
return { 'methods' => fields } unless model
|
68
|
+
|
69
|
+
column_names = model.column_names.map(&:to_sym)
|
70
|
+
instance_methods = model.instance_methods
|
71
|
+
permitted_attributes = current_ability.permitted_attributes(:read, model)
|
72
|
+
is_permitted_all = column_names == permitted_attributes
|
73
|
+
|
74
|
+
fields_hash = { 'only' => [], 'methods' => [] }
|
75
|
+
|
76
|
+
fields.each_with_object(fields_hash) do |field, acc|
|
77
|
+
field_symbol = field.to_sym
|
78
|
+
|
79
|
+
next if !is_permitted_all && permitted_attributes.exclude?(field_symbol)
|
80
|
+
|
81
|
+
if model.columns_hash[field]
|
82
|
+
acc['only'] << field
|
83
|
+
elsif instance_methods.include?(field_symbol)
|
84
|
+
acc['methods'] << field
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param params [Hash]
|
90
|
+
# @param key [String]
|
91
|
+
# @return [Hash]
|
92
|
+
def find_key_in_params(params, key)
|
93
|
+
params = params['include']
|
94
|
+
|
95
|
+
return {} if params.blank?
|
96
|
+
return params[key] if params[key]
|
97
|
+
|
98
|
+
params.keys.reduce(nil) do |acc, k|
|
99
|
+
acc || find_key_in_params(params[k], key)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param params [Hash]
|
104
|
+
# @return [Hash]
|
105
|
+
def normalize_include_params(params)
|
106
|
+
case params
|
107
|
+
when Array
|
108
|
+
params.index_with do |_|
|
109
|
+
{ 'include' => {} }
|
110
|
+
end
|
111
|
+
when String
|
112
|
+
{ params => { 'include' => {} } }
|
113
|
+
when Hash
|
114
|
+
include_hash =
|
115
|
+
params.transform_values do |value|
|
116
|
+
normalize_include_params(value)
|
117
|
+
end
|
118
|
+
|
119
|
+
{ 'include' => include_hash }
|
120
|
+
else
|
121
|
+
raise ArgumentError, "Wrong include param type #{params.class}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# @param model [Class<ActiveRecord::Base>]
|
126
|
+
# @param includes_hash [Hash]
|
127
|
+
# @return [Hash]
|
128
|
+
def build_models_index(model, includes_hash)
|
129
|
+
default_index = {
|
130
|
+
model.name.underscore => model,
|
131
|
+
model.name.underscore.split('/').last => model
|
132
|
+
}
|
133
|
+
|
134
|
+
includes_hash.reduce(default_index) do |acc, (key, value)|
|
135
|
+
reflection = model.reflections[key]
|
136
|
+
|
137
|
+
next acc unless reflection
|
138
|
+
next acc if reflection.polymorphic?
|
139
|
+
|
140
|
+
acc[key] = reflection.klass
|
141
|
+
|
142
|
+
acc.merge(build_models_index(reflection.klass, value))
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# @param string_path [String]
|
147
|
+
# @return [Hash]
|
148
|
+
def build_hash_from_string_path(string_path)
|
149
|
+
string_path.split(',').reduce({}) do |accumulator, path|
|
150
|
+
hash = {}
|
151
|
+
|
152
|
+
path.split('.').reduce(hash) do |acc, part|
|
153
|
+
acc_hash = {}
|
154
|
+
|
155
|
+
acc[part] = acc_hash
|
156
|
+
|
157
|
+
acc_hash
|
158
|
+
end
|
159
|
+
|
160
|
+
accumulator.deep_merge(hash)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# @param params [Hash]
|
165
|
+
# @return [Boolean]
|
166
|
+
def limit_zero_params?(params)
|
167
|
+
params.dig(:page, :limit).then { |limit| limit.present? && limit.to_i.zero? }
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,20 @@
|
|
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
|
+
if params[:meta].to_s.include?('count')
|
12
|
+
meta[:count] =
|
13
|
+
rel.limit(nil).offset(nil).reorder(nil).select(rel.klass.arel_table[rel.klass.primary_key]).count
|
14
|
+
end
|
15
|
+
|
16
|
+
meta
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|