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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +32 -0
  4. data/Rakefile +20 -0
  5. data/app/controllers/motor/alerts_controller.rb +74 -0
  6. data/app/controllers/motor/api_base_controller.rb +25 -0
  7. data/app/controllers/motor/application_controller.rb +6 -0
  8. data/app/controllers/motor/assets_controller.rb +28 -0
  9. data/app/controllers/motor/configs_controller.rb +30 -0
  10. data/app/controllers/motor/dashboards_controller.rb +54 -0
  11. data/app/controllers/motor/data_controller.rb +102 -0
  12. data/app/controllers/motor/forms_controller.rb +54 -0
  13. data/app/controllers/motor/queries_controller.rb +54 -0
  14. data/app/controllers/motor/resource_methods_controller.rb +21 -0
  15. data/app/controllers/motor/resources_controller.rb +23 -0
  16. data/app/controllers/motor/run_queries_controller.rb +45 -0
  17. data/app/controllers/motor/schemas_controller.rb +11 -0
  18. data/app/controllers/motor/send_alerts_controller.rb +24 -0
  19. data/app/controllers/motor/tags_controller.rb +11 -0
  20. data/app/controllers/motor/ui_controller.rb +19 -0
  21. data/app/jobs/motor/alert_sending_job.rb +13 -0
  22. data/app/jobs/motor/application_job.rb +6 -0
  23. data/app/mailers/motor/alerts_mailer.rb +50 -0
  24. data/app/mailers/motor/application_mailer.rb +7 -0
  25. data/app/models/motor/alert.rb +22 -0
  26. data/app/models/motor/alert_lock.rb +7 -0
  27. data/app/models/motor/application_record.rb +8 -0
  28. data/app/models/motor/config.rb +7 -0
  29. data/app/models/motor/dashboard.rb +18 -0
  30. data/app/models/motor/form.rb +14 -0
  31. data/app/models/motor/query.rb +14 -0
  32. data/app/models/motor/resource.rb +7 -0
  33. data/app/models/motor/tag.rb +7 -0
  34. data/app/models/motor/taggable_tag.rb +8 -0
  35. data/app/views/layouts/motor/application.html.erb +14 -0
  36. data/app/views/motor/alerts_mailer/alert_email.html.erb +126 -0
  37. data/app/views/motor/ui/show.html.erb +1 -0
  38. data/config/routes.rb +60 -0
  39. data/lib/generators/motor/install_generator.rb +22 -0
  40. data/lib/generators/motor/migration.rb +17 -0
  41. data/lib/generators/motor/templates/install.rb +135 -0
  42. data/lib/motor-admin.rb +3 -0
  43. data/lib/motor.rb +47 -0
  44. data/lib/motor/active_record_utils.rb +7 -0
  45. data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
  46. data/lib/motor/active_record_utils/types.rb +54 -0
  47. data/lib/motor/admin.rb +12 -0
  48. data/lib/motor/alerts.rb +10 -0
  49. data/lib/motor/alerts/persistance.rb +84 -0
  50. data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
  51. data/lib/motor/alerts/scheduler.rb +30 -0
  52. data/lib/motor/api.rb +6 -0
  53. data/lib/motor/api_query.rb +22 -0
  54. data/lib/motor/api_query/build_json.rb +109 -0
  55. data/lib/motor/api_query/build_meta.rb +17 -0
  56. data/lib/motor/api_query/filter.rb +55 -0
  57. data/lib/motor/api_query/paginate.rb +18 -0
  58. data/lib/motor/api_query/search.rb +73 -0
  59. data/lib/motor/api_query/sort.rb +27 -0
  60. data/lib/motor/assets.rb +45 -0
  61. data/lib/motor/build_schema.rb +23 -0
  62. data/lib/motor/build_schema/find_display_column.rb +60 -0
  63. data/lib/motor/build_schema/load_from_rails.rb +176 -0
  64. data/lib/motor/build_schema/merge_schema_configs.rb +77 -0
  65. data/lib/motor/build_schema/persist_resource_configs.rb +208 -0
  66. data/lib/motor/build_schema/reorder_schema.rb +52 -0
  67. data/lib/motor/build_schema/utils.rb +17 -0
  68. data/lib/motor/dashboards.rb +8 -0
  69. data/lib/motor/dashboards/persistance.rb +63 -0
  70. data/lib/motor/forms.rb +8 -0
  71. data/lib/motor/forms/persistance.rb +63 -0
  72. data/lib/motor/hash_serializer.rb +21 -0
  73. data/lib/motor/queries.rb +10 -0
  74. data/lib/motor/queries/persistance.rb +63 -0
  75. data/lib/motor/queries/postgresql_exec_query.rb +28 -0
  76. data/lib/motor/queries/run_query.rb +68 -0
  77. data/lib/motor/tags.rb +31 -0
  78. data/lib/motor/ui_configs.rb +62 -0
  79. data/lib/motor/version.rb +5 -0
  80. data/ui/dist/fonts/ionicons.woff2 +0 -0
  81. data/ui/dist/main-46621a8bdbb789e17c3f.css.gz +0 -0
  82. data/ui/dist/main-46621a8bdbb789e17c3f.js.gz +0 -0
  83. data/ui/dist/manifest.json +13 -0
  84. metadata +237 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class QueriesController < ApiBaseController
5
+ load_and_authorize_resource :query, only: %i[index show update destroy]
6
+
7
+ before_action :build_query, only: :create
8
+ authorize_resource :query, only: :create
9
+
10
+ def index
11
+ render json: { data: Motor::ApiQuery::BuildJson.call(@queries.active, params) }
12
+ end
13
+
14
+ def show
15
+ render json: { data: Motor::ApiQuery::BuildJson.call(@query, params) }
16
+ end
17
+
18
+ def create
19
+ if Motor::Queries::Persistance.name_already_exists?(@query)
20
+ render json: { errors: [{ source: 'name', detail: 'Name already exists' }] }, status: :unprocessable_entity
21
+ else
22
+ ApplicationRecord.transaction { @query.save! }
23
+
24
+ render json: { data: Motor::ApiQuery::BuildJson.call(@query, params) }
25
+ end
26
+ rescue ActiveRecord::RecordNotUnique
27
+ retry
28
+ end
29
+
30
+ def update
31
+ Motor::Queries::Persistance.update_from_params!(@query, query_params)
32
+
33
+ render json: { data: Motor::ApiQuery::BuildJson.call(@query, params) }
34
+ rescue Motor::Queries::Persistance::NameAlreadyExists
35
+ render json: { errors: [{ source: 'name', detail: 'Name already exists' }] }, status: :unprocessable_entity
36
+ end
37
+
38
+ def destroy
39
+ @query.update!(deleted_at: Time.current)
40
+
41
+ head :ok
42
+ end
43
+
44
+ private
45
+
46
+ def build_query
47
+ @query = Motor::Queries::Persistance.build_from_params(query_params)
48
+ end
49
+
50
+ def query_params
51
+ params.require(:data).permit(:name, :sql_body, :description, preferences: {}, tags: [])
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ResourceMethodsController < ApiBaseController
5
+ before_action :authorize_resource
6
+
7
+ def show
8
+ render json: { data: ActiveRecordUtils::FetchMethods.call(resource_class) }
9
+ end
10
+
11
+ private
12
+
13
+ def resource_class
14
+ @resource_class ||= Motor::BuildSchema::Utils.classify_slug(params[:resource])
15
+ end
16
+
17
+ def authorize_resource
18
+ authorize!(resource_class, :manage)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ResourcesController < ApiBaseController
5
+ load_and_authorize_resource
6
+
7
+ def index
8
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resources, params) }
9
+ end
10
+
11
+ def create
12
+ Motor::BuildSchema::PersistResourceConfigs.call(@resource)
13
+
14
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
15
+ end
16
+
17
+ private
18
+
19
+ def resource_params
20
+ params.require(:data).permit!
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class RunQueriesController < ApiBaseController
5
+ load_and_authorize_resource :query, only: :show, parent: false
6
+
7
+ before_action :build_query, only: :create
8
+ authorize_resource :query, only: :create
9
+
10
+ rescue_from 'ActiveRecord::StatementInvalid' do |e|
11
+ render json: { errors: [{ detail: e.message }] }, status: :unprocessable_entity
12
+ end
13
+
14
+ def show
15
+ render json: query_result_hash(query_result)
16
+ end
17
+
18
+ def create
19
+ render json: query_result_hash(query_result)
20
+ end
21
+
22
+ private
23
+
24
+ def query_result
25
+ Queries::RunQuery.call(@query, variables_hash: params[:variables])
26
+ end
27
+
28
+ def query_result_hash(query_result)
29
+ {
30
+ data: query_result.data,
31
+ meta: {
32
+ columns: query_result.columns
33
+ }
34
+ }
35
+ end
36
+
37
+ def build_query
38
+ @query = Motor::Queries::Persistance.build_from_params(query_params)
39
+ end
40
+
41
+ def query_params
42
+ params.require(:data).permit(:sql_body, preferences: {})
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class SchemasController < ApiBaseController
5
+ def show
6
+ render json: Motor::BuildSchema.call
7
+ end
8
+
9
+ def update; end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class SendAlertsController < ApiBaseController
5
+ before_action :build_alert, only: :create
6
+ authorize_resource :alert, only: :create
7
+
8
+ def create
9
+ AlertsMailer.alert_email(@alert).deliver_now!
10
+
11
+ head :ok
12
+ end
13
+
14
+ private
15
+
16
+ def build_alert
17
+ @alert = Motor::Alerts::Persistance.build_from_params(alert_params)
18
+ end
19
+
20
+ def alert_params
21
+ params.require(:data).permit(:query_id, :name, :to_emails)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class TagsController < ApiBaseController
5
+ load_and_authorize_resource :tag
6
+
7
+ def index
8
+ render json: { data: @tags }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class UiController < ApplicationController
5
+ layout 'motor/application'
6
+
7
+ def index
8
+ Motor.reload! if Motor.development?
9
+
10
+ render :show
11
+ end
12
+
13
+ def show
14
+ Motor.reload! if Motor.development?
15
+
16
+ render :show
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class AlertSendingJob < ApplicationJob
5
+ def perform(alert)
6
+ Motor::AlertLock.create!(alert_id: alert.id, lock_timestamp: alert.cron.previous_time.to_i)
7
+
8
+ Motor::AlertsMailer.alert_email(alert).deliver_now!
9
+ rescue ActiveRecord::RecordNotUnique
10
+ nil
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class AlertsMailer < ApplicationMailer
5
+ def alert_email(alert)
6
+ @alert = alert
7
+ @query_result = Queries::RunQuery.call(alert.query)
8
+
9
+ return if @alert.preferences[:send_empty].blank? && @query_result.data.blank?
10
+
11
+ attachments["#{alert.name.presence || 'data'}.csv"] = generate_csv(@query_result)
12
+
13
+ mail(
14
+ from: from_address,
15
+ to: alert.to_emails,
16
+ subject: alert.name.presence || @alert.query.name
17
+ )
18
+ end
19
+
20
+ private
21
+
22
+ def generate_csv(_query_result)
23
+ rows = [@query_result.columns.pluck(:name)] + @query_result.data
24
+
25
+ rows.map(&:to_csv).join
26
+ end
27
+
28
+ def from_address
29
+ from = ENV['MOTOR_ADMIN_FROM_ADDRESS'].presence
30
+
31
+ from ||= application_mailer_default_from
32
+ from ||= mailer_config_from_address
33
+ from ||= "reports@#{ENV['HOST'].sub(/\Awww\./, '')}" if ENV['HOST'].present?
34
+
35
+ from
36
+ end
37
+
38
+ def application_mailer_default_from
39
+ return if !defined?(::ApplicationMailer) || ::ApplicationMailer.default[:from].to_s.include?('example.com')
40
+
41
+ ::ApplicationMailer.default[:from].presence
42
+ end
43
+
44
+ def mailer_config_from_address
45
+ return if Rails.application.config.action_mailer.default_url_options[:host].blank?
46
+
47
+ "reports@#{Rails.application.config.action_mailer.default_url_options[:host].sub(/\Awww\./, '')}"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ApplicationMailer < ActionMailer::Base
5
+ layout 'mailer'
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Alert < ApplicationRecord
5
+ belongs_to :query
6
+ belongs_to :author, polymorphic: true, optional: true
7
+
8
+ has_many :alert_locks
9
+ has_many :taggable_tags, as: :taggable
10
+ has_many :tags, through: :taggable_tags, class_name: 'Motor::Tag'
11
+
12
+ serialize :preferences, HashSerializer
13
+
14
+ scope :active, -> { where(deleted_at: nil) }
15
+ scope :enabled, -> { where(is_enabled: true) }
16
+
17
+ def cron
18
+ @cron ||=
19
+ Fugit::Nat.parse("#{preferences[:interval]} #{ActiveSupport::TimeZone::MAPPING[preferences[:timezone]]}")
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class AlertLock < ApplicationRecord
5
+ belongs_to :alert
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ self.table_name_prefix = 'motor_'
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Config < ApplicationRecord
5
+ serialize :value, HashSerializer
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Dashboard < ApplicationRecord
5
+ belongs_to :author, polymorphic: true, optional: true
6
+
7
+ has_many :taggable_tags, as: :taggable
8
+ has_many :tags, through: :taggable_tags, class_name: 'Motor::Tag'
9
+
10
+ serialize :preferences, HashSerializer
11
+
12
+ scope :active, -> { where(deleted_at: nil) }
13
+
14
+ def queries
15
+ Motor::Query.where(id: preferences[:layout].pluck(:query_id))
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Form < ApplicationRecord
5
+ belongs_to :author, polymorphic: true, optional: true
6
+
7
+ has_many :taggable_tags, as: :taggable
8
+ has_many :tags, through: :taggable_tags, class_name: 'Motor::Tag'
9
+
10
+ serialize :preferences, HashSerializer
11
+
12
+ scope :active, -> { where(deleted_at: nil) }
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Query < ApplicationRecord
5
+ belongs_to :author, polymorphic: true, optional: true
6
+
7
+ has_many :taggable_tags, as: :taggable
8
+ has_many :tags, through: :taggable_tags, class_name: 'Motor::Tag'
9
+
10
+ serialize :preferences, HashSerializer
11
+
12
+ scope :active, -> { where(deleted_at: nil) }
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Resource < ApplicationRecord
5
+ serialize :preferences, HashSerializer
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class Tag < ApplicationRecord
5
+ has_many :taggable_tags
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class TaggableTag < ApplicationRecord
5
+ belongs_to :tag
6
+ belongs_to :taggable, polymorphic: true
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Motor Admin</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <link rel="stylesheet" media="all" href="<%= Motor::Assets.asset_path('main.css') %>">
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+ </head>
10
+ <body>
11
+ <%= yield %>
12
+ <script src="<%= Motor::Assets.asset_path('main.js') %>"></script>
13
+ </body>
14
+ </html>