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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +32 -0
- data/Rakefile +20 -0
- data/app/controllers/motor/alerts_controller.rb +74 -0
- data/app/controllers/motor/api_base_controller.rb +25 -0
- data/app/controllers/motor/application_controller.rb +6 -0
- data/app/controllers/motor/assets_controller.rb +28 -0
- data/app/controllers/motor/configs_controller.rb +30 -0
- data/app/controllers/motor/dashboards_controller.rb +54 -0
- data/app/controllers/motor/data_controller.rb +102 -0
- data/app/controllers/motor/forms_controller.rb +54 -0
- data/app/controllers/motor/queries_controller.rb +54 -0
- data/app/controllers/motor/resource_methods_controller.rb +21 -0
- data/app/controllers/motor/resources_controller.rb +23 -0
- data/app/controllers/motor/run_queries_controller.rb +45 -0
- data/app/controllers/motor/schemas_controller.rb +11 -0
- data/app/controllers/motor/send_alerts_controller.rb +24 -0
- data/app/controllers/motor/tags_controller.rb +11 -0
- data/app/controllers/motor/ui_controller.rb +19 -0
- data/app/jobs/motor/alert_sending_job.rb +13 -0
- data/app/jobs/motor/application_job.rb +6 -0
- data/app/mailers/motor/alerts_mailer.rb +50 -0
- data/app/mailers/motor/application_mailer.rb +7 -0
- data/app/models/motor/alert.rb +22 -0
- data/app/models/motor/alert_lock.rb +7 -0
- data/app/models/motor/application_record.rb +8 -0
- data/app/models/motor/config.rb +7 -0
- data/app/models/motor/dashboard.rb +18 -0
- data/app/models/motor/form.rb +14 -0
- data/app/models/motor/query.rb +14 -0
- data/app/models/motor/resource.rb +7 -0
- data/app/models/motor/tag.rb +7 -0
- data/app/models/motor/taggable_tag.rb +8 -0
- data/app/views/layouts/motor/application.html.erb +14 -0
- data/app/views/motor/alerts_mailer/alert_email.html.erb +126 -0
- data/app/views/motor/ui/show.html.erb +1 -0
- data/config/routes.rb +60 -0
- data/lib/generators/motor/install_generator.rb +22 -0
- data/lib/generators/motor/migration.rb +17 -0
- data/lib/generators/motor/templates/install.rb +135 -0
- data/lib/motor-admin.rb +3 -0
- data/lib/motor.rb +47 -0
- data/lib/motor/active_record_utils.rb +7 -0
- data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
- data/lib/motor/active_record_utils/types.rb +54 -0
- data/lib/motor/admin.rb +12 -0
- data/lib/motor/alerts.rb +10 -0
- data/lib/motor/alerts/persistance.rb +84 -0
- data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
- data/lib/motor/alerts/scheduler.rb +30 -0
- data/lib/motor/api.rb +6 -0
- data/lib/motor/api_query.rb +22 -0
- data/lib/motor/api_query/build_json.rb +109 -0
- data/lib/motor/api_query/build_meta.rb +17 -0
- data/lib/motor/api_query/filter.rb +55 -0
- data/lib/motor/api_query/paginate.rb +18 -0
- data/lib/motor/api_query/search.rb +73 -0
- data/lib/motor/api_query/sort.rb +27 -0
- data/lib/motor/assets.rb +45 -0
- data/lib/motor/build_schema.rb +23 -0
- data/lib/motor/build_schema/find_display_column.rb +60 -0
- data/lib/motor/build_schema/load_from_rails.rb +176 -0
- data/lib/motor/build_schema/merge_schema_configs.rb +77 -0
- data/lib/motor/build_schema/persist_resource_configs.rb +208 -0
- data/lib/motor/build_schema/reorder_schema.rb +52 -0
- data/lib/motor/build_schema/utils.rb +17 -0
- data/lib/motor/dashboards.rb +8 -0
- data/lib/motor/dashboards/persistance.rb +63 -0
- data/lib/motor/forms.rb +8 -0
- data/lib/motor/forms/persistance.rb +63 -0
- data/lib/motor/hash_serializer.rb +21 -0
- data/lib/motor/queries.rb +10 -0
- data/lib/motor/queries/persistance.rb +63 -0
- data/lib/motor/queries/postgresql_exec_query.rb +28 -0
- data/lib/motor/queries/run_query.rb +68 -0
- data/lib/motor/tags.rb +31 -0
- data/lib/motor/ui_configs.rb +62 -0
- data/lib/motor/version.rb +5 -0
- data/ui/dist/fonts/ionicons.woff2 +0 -0
- data/ui/dist/main-46621a8bdbb789e17c3f.css.gz +0 -0
- data/ui/dist/main-46621a8bdbb789e17c3f.js.gz +0 -0
- data/ui/dist/manifest.json +13 -0
- metadata +237 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
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,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,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,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,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,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,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>
|