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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2d6f58a141ea2313b151ee2648a4b98ea3327b8f81b5d7473d8f4a36e6ffb7a4
4
+ data.tar.gz: b30868890700641a08cff0abc865e736823def3b066ca8bf38c4968dddd3e2b4
5
+ SHA512:
6
+ metadata.gz: cc33e35f237bc0d15c57da84d67276efa2b26bc09d6b19845a1f6438f81c4e42ec0d5a64274aecaa817faacbde7d5f5c11245b3a0c81f97e3c3e99c808e8b7ec
7
+ data.tar.gz: 44152701924a0f006ed102080b612db45a9ca76caf6180c58ad04ec4eb6f8e2210e667a9ceec5228d7a9854a77fca67bf86d0660b655c7715bd71875e4d4a4d7
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2021 Pete Matsyburka
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Motor
2
+
3
+ Admin UI and Business Analytics.
4
+
5
+ ## Usage
6
+ How to use my plugin.
7
+
8
+ ## Installation
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'motor-admin'
13
+ ```
14
+
15
+ And then execute:
16
+ ```bash
17
+ $ bundle install
18
+ ```
19
+
20
+ Or install it yourself as:
21
+ ```bash
22
+ $ gem install motor-admin
23
+ ```
24
+
25
+ Create and run migration:
26
+ ```bash
27
+ $ rails generate motor:install && rake db:migrate
28
+ ```
29
+
30
+ ## License
31
+
32
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
+
8
+ load 'rails/tasks/statistics.rake'
9
+
10
+ require 'bundler/gem_tasks'
11
+
12
+ require 'rake/testtask'
13
+
14
+ Rake::TestTask.new(:test) do |t|
15
+ t.libs << 'test'
16
+ t.pattern = 'test/**/*_test.rb'
17
+ t.verbose = false
18
+ end
19
+
20
+ task default: :test
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class AlertsController < ApiBaseController
5
+ load_and_authorize_resource :alert, only: %i[index show update destroy]
6
+
7
+ before_action :build_alert, only: :create
8
+ authorize_resource :alert, only: :create
9
+
10
+ def index
11
+ render json: { data: Motor::ApiQuery::BuildJson.call(@alerts.active, params) }
12
+ end
13
+
14
+ def show
15
+ render json: { data: Motor::ApiQuery::BuildJson.call(@alert, params) }
16
+ end
17
+
18
+ def create
19
+ ApplicationRecord.transaction { @alert.save! }
20
+ Motor::Alerts::ScheduledAlertsCache.clear
21
+
22
+ render json: { data: Motor::ApiQuery::BuildJson.call(@alert, params) }
23
+ rescue Motor::Alerts::Persistance::NameAlreadyExists
24
+ name_already_exists_response
25
+ rescue Motor::Alerts::Persistance::InvalidInterval
26
+ invalid_interval_response
27
+ end
28
+
29
+ def update
30
+ Motor::Alerts::Persistance.update_from_params!(@alert, alert_params)
31
+ Motor::Alerts::ScheduledAlertsCache.clear
32
+
33
+ render json: { data: Motor::ApiQuery::BuildJson.call(@alert, params) }
34
+ rescue Motor::Alerts::Persistance::NameAlreadyExists
35
+ name_already_exists_response
36
+ rescue Motor::Alerts::Persistance::InvalidInterval
37
+ invalid_interval_response
38
+ end
39
+
40
+ def destroy
41
+ @alert.update!(deleted_at: Time.current)
42
+
43
+ head :ok
44
+ end
45
+
46
+ private
47
+
48
+ def name_already_exists_response
49
+ render json: { errors: [{ source: 'name', detail: 'Name already exists' }] },
50
+ status: :unprocessable_entity
51
+ end
52
+
53
+ def invalid_interval_response
54
+ render json: { errors: [{ source: 'preferences.interval', detail: 'Invalid interval' }] },
55
+ status: :unprocessable_entity
56
+ end
57
+
58
+ def build_alert
59
+ @alert = Motor::Alerts::Persistance.build_from_params(alert_params)
60
+ end
61
+
62
+ def alert_params
63
+ params.require(:data).permit(
64
+ :query_id,
65
+ :name,
66
+ :description,
67
+ :to_emails,
68
+ :is_enabled,
69
+ preferences: {},
70
+ tags: []
71
+ )
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ApiBaseController < ActionController::API
5
+ def current_user
6
+ if defined?(current_admin)
7
+ current_admin
8
+ elsif defined?(current_admin_user)
9
+ current_admin_user
10
+ elsif defined?(super)
11
+ super
12
+ end
13
+ end
14
+
15
+ def current_ability
16
+ klass = Class.new
17
+ klass.include(CanCan::Ability)
18
+ klass.define_method(:initialize) do |_user|
19
+ can :manage, :all
20
+ end
21
+
22
+ klass.new(current_user)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class AssetsController < ActionController::Metal
5
+ CACHE_STORE = ActiveSupport::Cache::MemoryStore.new
6
+
7
+ GZIP_TYPES = [
8
+ 'application/javascript',
9
+ 'text/css'
10
+ ].freeze
11
+
12
+ def show
13
+ filename = params[:filename]
14
+
15
+ return [404, {}, ''] unless Motor::Assets.manifest.values.include?(filename)
16
+
17
+ self.response_body = CACHE_STORE.fetch(filename) do
18
+ Motor::Assets.load_asset(filename)
19
+ end
20
+
21
+ headers['Content-Type'] = Marcel::MimeType.for(name: filename)
22
+
23
+ headers['Content-Encoding'] = 'gzip' if !Motor.development? && GZIP_TYPES.include?(headers['Content-Type'])
24
+
25
+ headers['Cache-Control'] = 'max-age=31536000'
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class ConfigsController < ApiBaseController
5
+ load_and_authorize_resource
6
+
7
+ def index
8
+ render json: { data: Motor::ApiQuery::BuildJson.call(@configs, params) }
9
+ end
10
+
11
+ def create
12
+ @config =
13
+ Motor::Config.find_or_initialize_by(key: @config.key).tap do |config|
14
+ config.value = @config.value
15
+ end
16
+
17
+ @config.save!
18
+
19
+ render json: { data: Motor::ApiQuery::BuildJson.call(@config, params) }
20
+ rescue ActiveRecord::RecordNotUnique
21
+ retry
22
+ end
23
+
24
+ private
25
+
26
+ def config_params
27
+ params.require(:data).permit!
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class DashboardsController < ApiBaseController
5
+ load_and_authorize_resource :dashboard, only: %i[index show update destroy]
6
+
7
+ before_action :build_dashboard, only: :create
8
+ authorize_resource :dashboard, only: :create
9
+
10
+ def index
11
+ render json: { data: Motor::ApiQuery::BuildJson.call(@dashboards.active, params) }
12
+ end
13
+
14
+ def show
15
+ render json: { data: Motor::ApiQuery::BuildJson.call(@dashboard, params) }
16
+ end
17
+
18
+ def create
19
+ if Motor::Dashboards::Persistance.title_already_exists?(@dashboard)
20
+ render json: { errors: [{ source: 'title', detail: 'Title already exists' }] }, status: :unprocessable_entity
21
+ else
22
+ ApplicationRecord.transaction { @dashboard.save! }
23
+
24
+ render json: { data: Motor::ApiQuery::BuildJson.call(@dashboard, params) }
25
+ end
26
+ rescue ActiveRecord::RecordNotUnique
27
+ retry
28
+ end
29
+
30
+ def update
31
+ Motor::Dashboards::Persistance.update_from_params!(@dashboard, dashboard_params)
32
+
33
+ render json: { data: Motor::ApiQuery::BuildJson.call(@dashboard, params) }
34
+ rescue Motor::Dashboards::Persistance::TitleAlreadyExists
35
+ render json: { errors: [{ source: 'title', detail: 'Title already exists' }] }, status: :unprocessable_entity
36
+ end
37
+
38
+ def destroy
39
+ @dashboard.update!(deleted_at: Time.current)
40
+
41
+ head :ok
42
+ end
43
+
44
+ private
45
+
46
+ def build_dashboard
47
+ @dashboard = Motor::Dashboards::Persistance.build_from_params(dashboard_params)
48
+ end
49
+
50
+ def dashboard_params
51
+ params.require(:data).permit(:title, :description, preferences: {}, tags: [])
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class DataController < ApiBaseController
5
+ INSTANCE_VARIABLE_NAME = 'resource'
6
+
7
+ before_action :load_and_authorize_resource
8
+ before_action :load_and_authorize_association
9
+
10
+ def index
11
+ @resources = Motor::ApiQuery.call(@resources, params)
12
+
13
+ render json: {
14
+ data: Motor::ApiQuery::BuildJson.call(@resources, params),
15
+ meta: Motor::ApiQuery::BuildMeta.call(@resources, params)
16
+ }
17
+ end
18
+
19
+ def show
20
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
21
+ end
22
+
23
+ def create
24
+ @resource.save!
25
+
26
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
27
+ end
28
+
29
+ def update
30
+ @resource.update!(resource_params)
31
+
32
+ render json: { data: Motor::ApiQuery::BuildJson.call(@resource, params) }
33
+ end
34
+
35
+ def destroy
36
+ if @resource.respond_to?(:deleted_at)
37
+ @resource.update(deleted_at: Time.current)
38
+ else
39
+ @resource.destroy!
40
+ end
41
+
42
+ head :ok
43
+ end
44
+
45
+ def execute
46
+ @resource.public_send(params[:method].to_sym)
47
+
48
+ head :ok
49
+ rescue StandardError => e
50
+ render json: { message: e.message }, status: :unprocessable_entity
51
+ end
52
+
53
+ private
54
+
55
+ def resource_class
56
+ @resource_class ||= Motor::BuildSchema::Utils.classify_slug(params[:resource])
57
+ end
58
+
59
+ def load_and_authorize_resource
60
+ options = {
61
+ class: resource_class,
62
+ parent: false,
63
+ instance_name: INSTANCE_VARIABLE_NAME
64
+ }
65
+
66
+ if params[:resource_id].present?
67
+ options = options.merge(
68
+ parent: true,
69
+ id_param: :resource_id
70
+ )
71
+ end
72
+
73
+ CanCan::ControllerResource.new(
74
+ self,
75
+ options
76
+ ).load_and_authorize_resource
77
+ end
78
+
79
+ def load_and_authorize_association
80
+ return if params[:association].blank?
81
+
82
+ association = resource_class.reflections[params[:association]]
83
+
84
+ if association
85
+ CanCan::ControllerResource.new(
86
+ self,
87
+ class: association.klass,
88
+ parent: false,
89
+ through: :resource,
90
+ through_association: params[:association].to_sym,
91
+ instance_name: INSTANCE_VARIABLE_NAME
92
+ ).load_and_authorize_resource
93
+ else
94
+ render json: { message: 'Unknown association' }, status: :not_found
95
+ end
96
+ end
97
+
98
+ def resource_params
99
+ params.fetch(:data, {}).except(resource_class.primary_key).permit!
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class FormsController < ApiBaseController
5
+ load_and_authorize_resource :form, only: %i[index show update destroy]
6
+
7
+ before_action :build_form, only: :create
8
+ authorize_resource :form, only: :create
9
+
10
+ def index
11
+ render json: { data: Motor::ApiQuery::BuildJson.call(@forms.active, params) }
12
+ end
13
+
14
+ def show
15
+ render json: { data: Motor::ApiQuery::BuildJson.call(@form, params) }
16
+ end
17
+
18
+ def create
19
+ if Motor::Forms::Persistance.name_already_exists?(@form)
20
+ render json: { errors: [{ source: 'name', detail: 'Name already exists' }] }, status: :unprocessable_entity
21
+ else
22
+ ApplicationRecord.transaction { @form.save! }
23
+
24
+ render json: { data: Motor::ApiQuery::BuildJson.call(@form, params) }
25
+ end
26
+ rescue ActiveRecord::RecordNotUnique
27
+ retry
28
+ end
29
+
30
+ def update
31
+ Motor::Forms::Persistance.update_from_params!(@form, form_params)
32
+
33
+ render json: { data: Motor::ApiQuery::BuildJson.call(@form, params) }
34
+ rescue Motor::Forms::Persistance::NameAlreadyExists
35
+ render json: { errors: [{ source: 'name', detail: 'Name already exists' }] }, status: :unprocessable_entity
36
+ end
37
+
38
+ def destroy
39
+ @form.update!(deleted_at: Time.current)
40
+
41
+ head :ok
42
+ end
43
+
44
+ private
45
+
46
+ def build_form
47
+ @form = Motor::Forms::Persistance.build_from_params(form_params)
48
+ end
49
+
50
+ def form_params
51
+ params.require(:data).permit(:name, :description, :api_path, :http_method, preferences: {}, tags: [])
52
+ end
53
+ end
54
+ end