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