motor-admin 0.1.9

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