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
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module BuildUiAppTag
|
6
|
+
CACHE_STORE =
|
7
|
+
if Motor.development?
|
8
|
+
ActiveSupport::Cache::NullStore.new
|
9
|
+
else
|
10
|
+
ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
|
11
|
+
end
|
12
|
+
|
13
|
+
module_function
|
14
|
+
|
15
|
+
def call(current_user = nil, current_ability = nil, cache_keys: LoadFromCache.load_cache_keys)
|
16
|
+
CACHE_STORE.fetch(app_tag_cache_key(cache_keys, current_user, current_ability)) do
|
17
|
+
CACHE_STORE.clear
|
18
|
+
|
19
|
+
data = build_data(cache_keys, current_user, current_ability)
|
20
|
+
Motor::ApplicationController.helpers.tag.div('', id: 'app', data: data)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def app_tag_cache_key(cache_keys, current_user, current_ability)
|
25
|
+
key = "#{I18n.locale}#{cache_keys.hash}#{current_user&.id}#{current_ability&.rules_hash}"
|
26
|
+
|
27
|
+
key += Motor::DefineArModels.defined_models_schema_md5.to_s if defined?(Motor::DefineArModels)
|
28
|
+
|
29
|
+
if Rails.env.development?
|
30
|
+
key += Digest::MD5.hexdigest(ActiveRecord::Base.descendants.map(&:object_id).sort.join)
|
31
|
+
end
|
32
|
+
|
33
|
+
key
|
34
|
+
end
|
35
|
+
|
36
|
+
# rubocop:disable Metrics/AbcSize
|
37
|
+
# rubocop:disable Metrics/MethodLength
|
38
|
+
# @return [Hash]
|
39
|
+
def build_data(cache_keys = {}, current_user = nil, current_ability = nil)
|
40
|
+
configs_cache_key = cache_keys[:configs]
|
41
|
+
|
42
|
+
{ version: Motor::VERSION,
|
43
|
+
current_user: serialize_current_user(current_user),
|
44
|
+
current_rules: current_ability.serialized_rules,
|
45
|
+
audits_count: Motor::Audit.count,
|
46
|
+
i18n: i18n_data,
|
47
|
+
base_path: Motor::Admin.routes.url_helpers.motor_path,
|
48
|
+
cable_path: Motor::Admin.routes.url_helpers.try(:motor_cable_path),
|
49
|
+
admin_settings_path: Rails.application.routes.url_helpers.try(:admin_settings_general_path),
|
50
|
+
active_storage_direct_uploads_enabled: ENV['MOTOR_ACTIVE_STORAGE_DIRECT_UPLOADS_ENABLED'].present?,
|
51
|
+
schema: Motor::BuildSchema.call(cache_keys, current_ability),
|
52
|
+
header_links: header_links_data_hash(current_user, current_ability, configs_cache_key),
|
53
|
+
homepage_layout: homepage_layout_data_hash(configs_cache_key),
|
54
|
+
databases: database_names,
|
55
|
+
queries: queries_data_hash(build_cache_key(cache_keys, :queries, current_user, current_ability),
|
56
|
+
current_ability),
|
57
|
+
dashboards: dashboards_data_hash(build_cache_key(cache_keys, :dashboards, current_user, current_ability),
|
58
|
+
current_ability),
|
59
|
+
alerts: alerts_data_hash(build_cache_key(cache_keys, :alerts, current_user, current_ability),
|
60
|
+
current_ability),
|
61
|
+
forms: forms_data_hash(build_cache_key(cache_keys, :forms, current_user, current_ability), current_ability) }
|
62
|
+
end
|
63
|
+
# rubocop:enable Metrics/AbcSize
|
64
|
+
# rubocop:enable Metrics/MethodLength
|
65
|
+
|
66
|
+
def i18n_data
|
67
|
+
I18n.t('motor', default: I18n.t('motor', locale: :en))
|
68
|
+
end
|
69
|
+
|
70
|
+
def serialize_current_user(user)
|
71
|
+
return unless user
|
72
|
+
|
73
|
+
attrs = user.as_json(only: %i[id email first_name last_name])
|
74
|
+
|
75
|
+
attrs['role'] = user.role if user.respond_to?(:role)
|
76
|
+
attrs['role_names'] = user.role_names if user.respond_to?(:role_names)
|
77
|
+
|
78
|
+
attrs
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [String]
|
82
|
+
def build_cache_key(cache_keys, key, current_user, current_ability)
|
83
|
+
"#{cache_keys[key].hash}#{current_user&.id}#{current_ability&.rules_hash}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def header_links_data_hash(current_user, current_ability, cache_key = nil)
|
87
|
+
configs = Motor::Configs::LoadFromCache.load_configs(cache_key: cache_key)
|
88
|
+
|
89
|
+
links = configs.find { |c| c.key == 'header.links' }&.value || []
|
90
|
+
links = add_default_links(links)
|
91
|
+
|
92
|
+
return links unless current_user
|
93
|
+
return links if current_ability.can?(:manage, :all)
|
94
|
+
|
95
|
+
filter_links_for_user(current_user, links)
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_default_links(links)
|
99
|
+
new_links = links.clone
|
100
|
+
|
101
|
+
unless links.find { |l| l['link_type'] == 'forms' }
|
102
|
+
new_links.unshift({ 'name' => I18n.t('motor.forms'), 'link_type' => 'forms' })
|
103
|
+
end
|
104
|
+
|
105
|
+
unless links.find { |l| l['link_type'] == 'reports' }
|
106
|
+
new_links.unshift({ 'name' => I18n.t('motor.reports'), 'link_type' => 'reports' })
|
107
|
+
end
|
108
|
+
|
109
|
+
new_links
|
110
|
+
end
|
111
|
+
|
112
|
+
def filter_links_for_user(current_user, links)
|
113
|
+
links.select do |link|
|
114
|
+
conditions = link['conditions']
|
115
|
+
|
116
|
+
next true if conditions.blank?
|
117
|
+
|
118
|
+
conditions.all? do |cond|
|
119
|
+
field_name =
|
120
|
+
if cond['field'] == 'role' && !current_user.respond_to?(:role)
|
121
|
+
:role_names
|
122
|
+
else
|
123
|
+
cond['field'].to_sym
|
124
|
+
end
|
125
|
+
|
126
|
+
next false unless field_name.in?(%i[email role role_names])
|
127
|
+
next false unless current_user.respond_to?(field_name)
|
128
|
+
|
129
|
+
Array.wrap(current_user.public_send(field_name)).intersection(Array.wrap(cond['value'])).present?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def homepage_layout_data_hash(cache_key = nil)
|
135
|
+
configs = Motor::Configs::LoadFromCache.load_configs(cache_key: cache_key)
|
136
|
+
|
137
|
+
configs.find { |c| c.key == 'homepage.layout' }&.value || []
|
138
|
+
end
|
139
|
+
|
140
|
+
def database_names
|
141
|
+
if defined?(Motor::EncryptedConfig)
|
142
|
+
Motor::DatabaseClasses.constants.map { |e| e.to_s.titleize }
|
143
|
+
elsif ActiveRecord::Base.configurations.try(:configurations)
|
144
|
+
ActiveRecord::Base.configurations.configurations
|
145
|
+
.select { |c| c.env_name == Rails.env }
|
146
|
+
.map { |c| c.try(:name) || c.try(:spec_name) }.compact
|
147
|
+
else
|
148
|
+
['primary']
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def queries_data_hash(cache_key = nil, current_ability = nil)
|
153
|
+
Motor::Configs::LoadFromCache.load_queries(cache_key: cache_key, current_ability: current_ability)
|
154
|
+
.as_json(only: %i[id name updated_at],
|
155
|
+
include: { tags: { only: %i[id name] } })
|
156
|
+
end
|
157
|
+
|
158
|
+
def dashboards_data_hash(cache_key = nil, current_ability = nil)
|
159
|
+
Motor::Configs::LoadFromCache.load_dashboards(cache_key: cache_key, current_ability: current_ability)
|
160
|
+
.as_json(only: %i[id title updated_at],
|
161
|
+
include: { tags: { only: %i[id name] } })
|
162
|
+
end
|
163
|
+
|
164
|
+
def alerts_data_hash(cache_key = nil, current_ability = nil)
|
165
|
+
Motor::Configs::LoadFromCache.load_alerts(cache_key: cache_key, current_ability: current_ability)
|
166
|
+
.as_json(only: %i[id name is_enabled updated_at],
|
167
|
+
include: { tags: { only: %i[id name] } })
|
168
|
+
end
|
169
|
+
|
170
|
+
def forms_data_hash(cache_key = nil, current_ability = nil)
|
171
|
+
Motor::Configs::LoadFromCache.load_forms(cache_key: cache_key, current_ability: current_ability)
|
172
|
+
.as_json(only: %i[id name updated_at],
|
173
|
+
include: { tags: { only: %i[id name] } })
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module LoadFromCache
|
6
|
+
CACHE_HASH = ActiveSupport::HashWithIndifferentAccess.new
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def call
|
11
|
+
cache_keys = load_cache_keys
|
12
|
+
|
13
|
+
{
|
14
|
+
configs: load_configs(cache_key: cache_keys[:configs]),
|
15
|
+
resources: load_resources(cache_key: cache_keys[:resources]),
|
16
|
+
queries: load_queries(cache_key: cache_keys[:queries]),
|
17
|
+
dashboards: load_dashboards(cache_key: cache_keys[:dashboards]),
|
18
|
+
alerts: load_alerts(cache_key: cache_keys[:alerts]),
|
19
|
+
forms: load_forms(cache_key: cache_keys[:forms])
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def load_configs(cache_key: nil)
|
24
|
+
maybe_fetch_from_cache('configs', cache_key) do
|
25
|
+
Motor::Config.all.load
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def load_resources(cache_key: nil)
|
30
|
+
maybe_fetch_from_cache('resources', cache_key) do
|
31
|
+
Motor::Resource.all.load
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_queries(cache_key: nil, current_ability: nil)
|
36
|
+
maybe_fetch_from_cache('queries', cache_key) do
|
37
|
+
rel = Motor::Query.all.active.preload(:tags)
|
38
|
+
rel = rel.accessible_by(current_ability) if current_ability
|
39
|
+
|
40
|
+
rel.load
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def load_dashboards(cache_key: nil, current_ability: nil)
|
45
|
+
maybe_fetch_from_cache('dashboards', cache_key) do
|
46
|
+
rel = Motor::Dashboard.all.active.preload(:tags)
|
47
|
+
rel = rel.accessible_by(current_ability) if current_ability
|
48
|
+
|
49
|
+
rel.load
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def load_alerts(cache_key: nil, current_ability: nil)
|
54
|
+
maybe_fetch_from_cache('alerts', cache_key) do
|
55
|
+
rel = Motor::Alert.all.active.preload(:tags)
|
56
|
+
rel = rel.accessible_by(current_ability) if current_ability
|
57
|
+
|
58
|
+
rel.load
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_forms(cache_key: nil, current_ability: nil)
|
63
|
+
maybe_fetch_from_cache('forms', cache_key) do
|
64
|
+
rel = Motor::Form.all.active.preload(:tags)
|
65
|
+
rel = rel.accessible_by(current_ability) if current_ability
|
66
|
+
|
67
|
+
rel.load
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def load_api_configs(cache_key: nil)
|
72
|
+
maybe_fetch_from_cache('forms', cache_key) do
|
73
|
+
Motor::ApiConfig.all.active.load
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def maybe_fetch_from_cache(type, cache_key)
|
78
|
+
return yield unless cache_key
|
79
|
+
|
80
|
+
if CACHE_HASH[type] && CACHE_HASH[type][:key] == cache_key
|
81
|
+
CACHE_HASH[type][:value]
|
82
|
+
else
|
83
|
+
result = yield
|
84
|
+
|
85
|
+
CACHE_HASH[type] = { key: cache_key, value: result }
|
86
|
+
|
87
|
+
result
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def load_cache_keys
|
92
|
+
result = ActiveRecord::Base.connection.exec_query(cache_keys_sql).rows
|
93
|
+
|
94
|
+
result.to_h.with_indifferent_access
|
95
|
+
end
|
96
|
+
|
97
|
+
def cache_keys_sql
|
98
|
+
[
|
99
|
+
Motor::Config.select("'configs', MAX(updated_at)").to_sql,
|
100
|
+
Motor::Resource.select("'resources', MAX(updated_at)").to_sql,
|
101
|
+
Motor::Dashboard.select("'dashboards', MAX(updated_at)").to_sql,
|
102
|
+
Motor::Alert.select("'alerts', MAX(updated_at)").to_sql,
|
103
|
+
Motor::Query.select("'queries', MAX(updated_at)").to_sql,
|
104
|
+
Motor::Form.select("'forms', MAX(updated_at)").to_sql,
|
105
|
+
Motor::ApiConfig.select("'api_configs', MAX(updated_at)").to_sql
|
106
|
+
].join(' UNION ')
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module SyncFromFile
|
6
|
+
MUTEXT = Mutex.new
|
7
|
+
FILE_TIMESTAMPS_STORE = ActiveSupport::Cache::MemoryStore.new(size: 1.megabyte)
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def call(with_exception: false)
|
12
|
+
MUTEXT.synchronize do
|
13
|
+
file = Pathname.new(Motor::Configs.file_path)
|
14
|
+
|
15
|
+
file_timestamp =
|
16
|
+
begin
|
17
|
+
file.ctime
|
18
|
+
rescue Errno::ENOENT
|
19
|
+
raise if with_exception
|
20
|
+
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
next unless file_timestamp
|
25
|
+
|
26
|
+
FILE_TIMESTAMPS_STORE.fetch(file_timestamp.to_s) do
|
27
|
+
Motor::Configs::SyncFromHash.call(
|
28
|
+
YAML.safe_load(file.read, permitted_classes: [Time, Date])
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module SyncFromHash
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(configs_hash)
|
9
|
+
return if configs_hash.blank?
|
10
|
+
|
11
|
+
configs_hash = configs_hash.with_indifferent_access
|
12
|
+
|
13
|
+
Motor::ApplicationRecord.transaction do
|
14
|
+
sync_queries(configs_hash)
|
15
|
+
sync_alerts(configs_hash)
|
16
|
+
sync_dashboards(configs_hash)
|
17
|
+
sync_forms(configs_hash)
|
18
|
+
sync_configs(configs_hash)
|
19
|
+
sync_resources(configs_hash)
|
20
|
+
sync_api_configs(configs_hash)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def sync_queries(configs_hash)
|
25
|
+
sync_taggable(
|
26
|
+
Motor::Configs::LoadFromCache.load_queries,
|
27
|
+
configs_hash[:queries],
|
28
|
+
configs_hash[:file_version],
|
29
|
+
Motor::Queries::Persistance.method(:update_from_params!)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def sync_alerts(configs_hash)
|
34
|
+
sync_taggable(
|
35
|
+
Motor::Configs::LoadFromCache.load_alerts,
|
36
|
+
configs_hash[:alerts],
|
37
|
+
configs_hash[:file_version],
|
38
|
+
Motor::Alerts::Persistance.method(:update_from_params!)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def sync_forms(configs_hash)
|
43
|
+
sync_taggable(
|
44
|
+
Motor::Configs::LoadFromCache.load_forms,
|
45
|
+
configs_hash[:forms],
|
46
|
+
configs_hash[:file_version],
|
47
|
+
Motor::Forms::Persistance.method(:update_from_params!)
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def sync_dashboards(configs_hash)
|
52
|
+
sync_taggable(
|
53
|
+
Motor::Configs::LoadFromCache.load_dashboards,
|
54
|
+
configs_hash[:dashboards],
|
55
|
+
configs_hash[:file_version],
|
56
|
+
Motor::Dashboards::Persistance.method(:update_from_params!)
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def sync_configs(configs_hash)
|
61
|
+
configs_index = Motor::Configs::LoadFromCache.load_configs.index_by(&:key)
|
62
|
+
|
63
|
+
configs_hash[:configs].each do |attrs|
|
64
|
+
record = configs_index[attrs[:key]] || Motor::Config.new
|
65
|
+
|
66
|
+
next if record.updated_at && attrs[:updated_at] <= record.updated_at
|
67
|
+
|
68
|
+
record.update!(attrs)
|
69
|
+
end
|
70
|
+
|
71
|
+
ActiveRecordUtils.reset_id_sequence!(Motor::Config)
|
72
|
+
end
|
73
|
+
|
74
|
+
def sync_api_configs(configs_hash)
|
75
|
+
return if configs_hash[:api_configs].blank?
|
76
|
+
|
77
|
+
configs_index = Motor::Configs::LoadFromCache.load_api_configs.index_by(&:name)
|
78
|
+
|
79
|
+
configs_hash[:api_configs].each do |attrs|
|
80
|
+
record = configs_index[attrs[:name]] || Motor::ApiConfig.new
|
81
|
+
|
82
|
+
next if record.updated_at && attrs[:updated_at] <= record.updated_at
|
83
|
+
|
84
|
+
record.update!(attrs.merge(deleted_at: nil))
|
85
|
+
end
|
86
|
+
|
87
|
+
archive_api_configs(configs_index, configs_hash[:api_configs])
|
88
|
+
|
89
|
+
ActiveRecordUtils.reset_id_sequence!(Motor::ApiConfig)
|
90
|
+
end
|
91
|
+
|
92
|
+
def archive_api_configs(configs_index, api_configs)
|
93
|
+
configs_index.except(*api_configs.pluck('name')).each_value do |config|
|
94
|
+
config.update!(deleted_at: Time.current) if config.deleted_at.blank?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def sync_resources(configs_hash)
|
99
|
+
resources_index = Motor::Configs::LoadFromCache.load_resources.index_by(&:name)
|
100
|
+
|
101
|
+
configs_hash[:resources].each do |attrs|
|
102
|
+
record = resources_index.fetch(attrs[:name], Motor::Resource.new)
|
103
|
+
|
104
|
+
next if record.updated_at && attrs[:updated_at] < record.updated_at
|
105
|
+
next if record.updated_at &&
|
106
|
+
attrs[:updated_at] == record.updated_at &&
|
107
|
+
attrs[:preferences] == record.preferences
|
108
|
+
|
109
|
+
record.updated_at_will_change!
|
110
|
+
record.update!(attrs)
|
111
|
+
end
|
112
|
+
|
113
|
+
ActiveRecordUtils.reset_id_sequence!(Motor::Resource)
|
114
|
+
end
|
115
|
+
|
116
|
+
def sync_taggable(records, config_items, configs_timestamp, update_proc)
|
117
|
+
processed_records, create_items = update_taggable_items(records, config_items, update_proc)
|
118
|
+
|
119
|
+
create_taggable_items(create_items, records.klass, update_proc)
|
120
|
+
|
121
|
+
archive_taggable_items(records - processed_records, configs_timestamp)
|
122
|
+
|
123
|
+
ActiveRecordUtils.reset_id_sequence!(records.klass)
|
124
|
+
end
|
125
|
+
|
126
|
+
def update_taggable_items(records, config_items, update_proc)
|
127
|
+
record_ids_hash = records.index_by(&:id)
|
128
|
+
|
129
|
+
config_items.each_with_object([[], []]) do |attrs, (processed_acc, create_acc)|
|
130
|
+
record = record_ids_hash[attrs[:id]]
|
131
|
+
|
132
|
+
next create_acc << attrs unless record
|
133
|
+
|
134
|
+
processed_acc << record if record
|
135
|
+
|
136
|
+
next if record.updated_at >= attrs[:updated_at]
|
137
|
+
|
138
|
+
update_proc.call(record, attrs, force_replace: true)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def create_taggable_items(create_items, records_class, update_proc)
|
143
|
+
create_items.each do |attrs|
|
144
|
+
record = records_class.find_or_initialize_by(id: attrs[:id]).tap { |e| e.deleted_at = nil }
|
145
|
+
|
146
|
+
update_proc.call(record, attrs, force_replace: true)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def archive_taggable_items(records_to_remove, configs_timestamp)
|
151
|
+
records_to_remove.each do |record|
|
152
|
+
next if record.updated_at > configs_timestamp
|
153
|
+
|
154
|
+
record.update!(deleted_at: Time.current) if record.deleted_at.blank?
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
class SyncMiddleware
|
6
|
+
KeyNotSpecified = Class.new(StandardError)
|
7
|
+
NotAuthenticated = Class.new(StandardError)
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
if env['PATH_INFO'] == Motor::Configs::SYNC_API_PATH
|
15
|
+
authenticate!(env['HTTP_X_AUTHORIZATION'])
|
16
|
+
|
17
|
+
case env['REQUEST_METHOD']
|
18
|
+
when 'GET'
|
19
|
+
respond_with_configs
|
20
|
+
when 'POST'
|
21
|
+
input = env['rack.input']
|
22
|
+
input.rewind
|
23
|
+
sync_configs(input.read)
|
24
|
+
else
|
25
|
+
@app.call(env)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
@app.call(env)
|
29
|
+
end
|
30
|
+
rescue NotAuthenticated
|
31
|
+
[403, {}, ['Invalid synchronization API key']]
|
32
|
+
rescue KeyNotSpecified
|
33
|
+
[404, {}, ['Set `MOTOR_SYNC_API_KEY` environment variable in order to sync configs']]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def authenticate!(token)
|
39
|
+
raise KeyNotSpecified if Motor::Configs::SYNC_ACCESS_KEY.blank?
|
40
|
+
raise NotAuthenticated if token.blank?
|
41
|
+
|
42
|
+
is_token_valid =
|
43
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
44
|
+
Digest::SHA256.hexdigest(token),
|
45
|
+
Digest::SHA256.hexdigest(Motor::Configs::SYNC_ACCESS_KEY)
|
46
|
+
)
|
47
|
+
|
48
|
+
raise NotAuthenticated unless is_token_valid
|
49
|
+
end
|
50
|
+
|
51
|
+
def respond_with_configs
|
52
|
+
[
|
53
|
+
200,
|
54
|
+
{ 'Content-Type' => 'application/json' },
|
55
|
+
[Motor::Configs::BuildConfigsHash.call.to_json]
|
56
|
+
]
|
57
|
+
rescue StandardError => e
|
58
|
+
[500, {}, [e.message]]
|
59
|
+
end
|
60
|
+
|
61
|
+
def sync_configs(body)
|
62
|
+
configs_hash = JSON.parse(body)
|
63
|
+
|
64
|
+
Motor::Configs::SyncFromHash.call(configs_hash)
|
65
|
+
|
66
|
+
[200, {}, []]
|
67
|
+
rescue StandardError => e
|
68
|
+
[500, {}, [e.message]]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module SyncWithRemote
|
6
|
+
UnableToSync = Class.new(StandardError)
|
7
|
+
ApiNotFound = Class.new(StandardError)
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def call(remote_url, api_key)
|
12
|
+
url = remote_url.delete_suffix('/') + Motor::Configs::SYNC_API_PATH
|
13
|
+
|
14
|
+
sync_from_remote!(url, api_key)
|
15
|
+
sync_to_remote!(url, api_key)
|
16
|
+
end
|
17
|
+
|
18
|
+
def sync_from_remote!(remote_url, api_key)
|
19
|
+
response = Motor::NetHttpUtils.get(remote_url, {}, { 'X-Authorization' => api_key })
|
20
|
+
|
21
|
+
raise ApiNotFound if response.is_a?(Net::HTTPNotFound)
|
22
|
+
raise UnableToSync, [response.message, response.body].join(': ') unless response.is_a?(Net::HTTPSuccess)
|
23
|
+
|
24
|
+
configs_hash = JSON.parse(response.body)
|
25
|
+
|
26
|
+
Motor::Configs::SyncFromHash.call(configs_hash)
|
27
|
+
end
|
28
|
+
|
29
|
+
def sync_to_remote!(remote_url, api_key)
|
30
|
+
configs_hash = Motor::Configs::BuildConfigsHash.call
|
31
|
+
|
32
|
+
response = Motor::NetHttpUtils.post(
|
33
|
+
remote_url,
|
34
|
+
{},
|
35
|
+
{
|
36
|
+
'X-Authorization' => api_key,
|
37
|
+
'Content-Type' => 'application/json'
|
38
|
+
},
|
39
|
+
configs_hash.to_json
|
40
|
+
)
|
41
|
+
|
42
|
+
raise ApiNotFound if response.is_a?(Net::HTTPNotFound)
|
43
|
+
raise UnableToSync, [response.message, response.body].join(': ') unless response.is_a?(Net::HTTPSuccess)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
module WriteToFile
|
6
|
+
THREAD_POOL = Concurrent::FixedThreadPool.new(1)
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def call
|
11
|
+
return if Rails.env.test?
|
12
|
+
return if THREAD_POOL.queue_length.positive?
|
13
|
+
|
14
|
+
THREAD_POOL.post do
|
15
|
+
ActiveRecord::Base.logger.silence do
|
16
|
+
write_with_lock
|
17
|
+
end
|
18
|
+
rescue StandardError => e
|
19
|
+
Rails.logger.error(e)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def write_with_lock
|
24
|
+
File.open(Motor::Configs.file_path, 'w') do |file|
|
25
|
+
file.flock(File::LOCK_EX)
|
26
|
+
|
27
|
+
YAML.dump(Motor::Configs::BuildConfigsHash.call, file)
|
28
|
+
|
29
|
+
file.flock(File::LOCK_UN)
|
30
|
+
|
31
|
+
file
|
32
|
+
end.close
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Configs
|
5
|
+
FILE_PATH = 'config/motor.yml'
|
6
|
+
SYNC_API_PATH = '/motor_configs_sync'
|
7
|
+
SYNC_ACCESS_KEY = ENV.fetch('MOTOR_SYNC_API_KEY', '')
|
8
|
+
WORKDIR_FILE_NAME = 'motor-admin.yml'
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def clear
|
13
|
+
Motor::Resource.destroy_all
|
14
|
+
Motor::Alert.destroy_all
|
15
|
+
Motor::Query.destroy_all
|
16
|
+
Motor::Dashboard.destroy_all
|
17
|
+
Motor::Form.destroy_all
|
18
|
+
Motor::ApiConfig.destroy_all
|
19
|
+
Motor::Config.destroy_all
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [String]
|
23
|
+
def file_path
|
24
|
+
if defined?(MotorAdmin::Application)
|
25
|
+
[ENV['WORKDIR'], WORKDIR_FILE_NAME].compact.join('/')
|
26
|
+
else
|
27
|
+
Rails.root.join(FILE_PATH).to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require_relative 'configs/load_from_cache'
|
34
|
+
require_relative 'configs/build_ui_app_tag'
|
35
|
+
require_relative 'configs/build_configs_hash'
|
36
|
+
require_relative 'configs/write_to_file'
|
37
|
+
require_relative 'configs/sync_from_hash'
|
38
|
+
require_relative 'configs/sync_from_file'
|
39
|
+
require_relative 'configs/sync_with_remote'
|