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,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module PersistResourceConfigs
6
+ RESOURCE_ATTRS = %w[display_name visible].freeze
7
+ COLUMN_ATTRS = %w[name display_name column_type access_type default_value virtual].freeze
8
+ ASSOCIATION_ATTRS = %w[name display_name visible].freeze
9
+ ACTION_ATTRS = %w[name display_name action_type preferences visible].freeze
10
+ TAB_ATTRS = %w[name display_name tab_type preferences visible].freeze
11
+
12
+ COLUMN_DEFAULTS = {
13
+ access_type: 'read_write',
14
+ default_value: nil,
15
+ validators: []
16
+ }.with_indifferent_access
17
+
18
+ ACTION_DEFAULTS = {
19
+ visible: true,
20
+ preferences: {}
21
+ }.with_indifferent_access
22
+
23
+ TAB_DEFAULTS = {
24
+ visible: true,
25
+ preferences: {}
26
+ }.with_indifferent_access
27
+
28
+ module_function
29
+
30
+ # @param resource [Motor::Resource]
31
+ # @return [Motor::Resource]
32
+ def call(resource)
33
+ preferences = resource.preferences
34
+
35
+ resource = Motor::Resource.find_or_initialize_by(name: resource.name)
36
+
37
+ assign_preferences!(resource, preferences)
38
+
39
+ resource.save!
40
+
41
+ resource
42
+ rescue ActiveRecord::RecordNotUnique
43
+ retry
44
+ end
45
+
46
+ # @param resource [Motor::Resource]
47
+ # @param preferences [HashWithIndifferentAccess]
48
+ # @return [Motor::Resource]
49
+ def assign_preferences!(resource, preferences)
50
+ default_schema = fetch_default_schema(resource.name)
51
+
52
+ resource.preferences = normalize_preferences(
53
+ default_schema,
54
+ resource.preferences,
55
+ preferences
56
+ )
57
+
58
+ resource
59
+ end
60
+
61
+ # @param default_prefs [HashWithIndifferentAccess]
62
+ # @param existing_prefs [HashWithIndifferentAccess]
63
+ # @param new_prefs [HashWithIndifferentAccess]
64
+ # @return [HashWithIndifferentAccess]
65
+ def normalize_preferences(default_prefs, existing_prefs, new_prefs)
66
+ normalized_preferences = new_prefs.slice(*RESOURCE_ATTRS).with_indifferent_access
67
+ normalized_preferences = existing_prefs.merge(normalized_preferences)
68
+ normalized_preferences = reject_default(default_prefs, normalized_preferences)
69
+
70
+ if new_prefs[:columns].present?
71
+ normalized_preferences[:columns] = normalize_columns(
72
+ default_prefs[:columns],
73
+ existing_prefs.fetch(:columns, []),
74
+ new_prefs.fetch(:columns, [])
75
+ )
76
+ end
77
+
78
+ if new_prefs[:associations].present?
79
+ normalized_preferences[:associations] = normalize_associations(
80
+ default_prefs[:associations],
81
+ existing_prefs.fetch(:associations, []),
82
+ new_prefs.fetch(:associations, [])
83
+ )
84
+ end
85
+
86
+ if new_prefs[:actions].present?
87
+ normalized_preferences[:actions] = normalize_actions(
88
+ default_prefs[:actions],
89
+ existing_prefs.fetch(:actions, []),
90
+ new_prefs.fetch(:actions, [])
91
+ )
92
+ end
93
+
94
+ if new_prefs[:tabs].present?
95
+ normalized_preferences[:tabs] = normalize_tabs(
96
+ default_prefs[:tabs],
97
+ existing_prefs.fetch(:tabs, []),
98
+ new_prefs.fetch(:tabs, [])
99
+ )
100
+ end
101
+
102
+ normalized_preferences.compact
103
+ end
104
+
105
+ # @param default_columns [Array<HashWithIndifferentAccess>]
106
+ # @param existing_columns [Array<HashWithIndifferentAccess>]
107
+ # @param new_columns [Array<HashWithIndifferentAccess>]
108
+ # @return [Array<HashWithIndifferentAccess>]
109
+ def normalize_columns(default_columns, existing_columns, new_columns)
110
+ (existing_columns.pluck(:name) + new_columns.pluck(:name)).uniq.map do |name|
111
+ new_column = safe_fetch_by_name(new_columns, name)
112
+
113
+ next if new_column[:_remove]
114
+
115
+ existing_column = safe_fetch_by_name(existing_columns, name)
116
+ default_column = safe_fetch_by_name(default_columns, name)
117
+ column_attrs = new_column.slice(*COLUMN_ATTRS)
118
+
119
+ normalized_column = existing_column.merge(column_attrs)
120
+ normalized_column = reject_default(default_column, normalized_column)
121
+
122
+ normalized_column.merge(name: name) if normalized_column.present?
123
+ end.compact.presence
124
+ end
125
+
126
+ # @param default_actions [Array<HashWithIndifferentAccess>]
127
+ # @param existing_actions [Array<HashWithIndifferentAccess>]
128
+ # @param new_actions [Array<HashWithIndifferentAccess>]
129
+ # @return [Array<HashWithIndifferentAccess>]
130
+ def normalize_actions(default_actions, existing_actions, new_actions)
131
+ (existing_actions.pluck(:name) + new_actions.pluck(:name)).uniq.map do |name|
132
+ new_action = safe_fetch_by_name(new_actions, name)
133
+
134
+ next if new_action[:_remove]
135
+
136
+ existing_action = safe_fetch_by_name(existing_actions, name)
137
+ default_action = safe_fetch_by_name(default_actions, name)
138
+ action_attrs = new_action.slice(*ACTION_ATTRS)
139
+
140
+ normalized_action = existing_action.merge(action_attrs)
141
+ normalized_action = reject_default(default_action.presence || TAB_DEFAULTS, normalized_action)
142
+
143
+ normalized_action.merge(name: name) if normalized_action.present?
144
+ end.compact.presence
145
+ end
146
+
147
+ # @param default_tabs [Array<HashWithIndifferentAccess>]
148
+ # @param existing_tabs [Array<HashWithIndifferentAccess>]
149
+ # @param new_tabs [Array<HashWithIndifferentAccess>]
150
+ # @return [Array<HashWithIndifferentAccess>]
151
+ def normalize_tabs(default_tabs, existing_tabs, new_tabs)
152
+ (existing_tabs.pluck(:name) + new_tabs.pluck(:name)).uniq.map do |name|
153
+ new_tab = safe_fetch_by_name(new_tabs, name)
154
+
155
+ next if new_tab[:_remove]
156
+
157
+ existing_tab = safe_fetch_by_name(existing_tabs, name)
158
+ default_tab = safe_fetch_by_name(default_tabs, name)
159
+ tab_attrs = new_tab.slice(*TAB_ATTRS)
160
+
161
+ normalized_tab = existing_tab.merge(tab_attrs)
162
+ normalized_tab = reject_default(default_tab.presence || TAB_DEFAULTS, normalized_tab)
163
+
164
+ normalized_tab.merge(name: name) if normalized_tab.present?
165
+ end.compact.presence
166
+ end
167
+
168
+ # @param default_assocs [Array<HashWithIndifferentAccess>]
169
+ # @param existing_assocs [Array<HashWithIndifferentAccess>]
170
+ # @param new_assocs [Array<HashWithIndifferentAccess>]
171
+ # @return [Array<HashWithIndifferentAccess>]
172
+ def normalize_associations(default_assocs, existing_assocs, new_assocs)
173
+ (existing_assocs.pluck(:name) + new_assocs.pluck(:name)).uniq.map do |name|
174
+ new_assoc = safe_fetch_by_name(new_assocs, name)
175
+ existing_assoc = safe_fetch_by_name(existing_assocs, name)
176
+ default_assoc = safe_fetch_by_name(default_assocs, name)
177
+ assoc_attrs = new_assoc.slice(*ASSOCIATION_ATTRS)
178
+
179
+ normalized_assoc = existing_assoc.merge(assoc_attrs)
180
+ normalized_assoc = reject_default(default_assoc, normalized_assoc)
181
+
182
+ normalized_assoc.merge(name: name) if normalized_assoc.present?
183
+ end.compact.presence
184
+ end
185
+
186
+ def safe_fetch_by_name(list, name)
187
+ list.find { |e| e[:name] == name } || {}
188
+ end
189
+
190
+ # @param resource_name [String]
191
+ # @return [HashWithIndifferentAccess]
192
+ def fetch_default_schema(resource_name)
193
+ LoadFromRails.build_model_schema(resource_name.classify.constantize)
194
+ end
195
+
196
+ # @param default [HashWithIndifferentAccess]
197
+ # @param new [HashWithIndifferentAccess]
198
+ # @return [HashWithIndifferentAccess]
199
+ def reject_default(default, new)
200
+ return new unless default
201
+
202
+ new.reject do |key, value|
203
+ default[key].to_json == value.to_json
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ReorderSchema
5
+ module_function
6
+
7
+ # @param schema [Array<HashWithIndifferentAccess>]
8
+ # @return [Array<HashWithIndifferentAccess>]
9
+ def call(schema)
10
+ configs = load_configs
11
+
12
+ schema = sort_by_name(schema, configs['resources.order'])
13
+
14
+ schema.map do |model|
15
+ columns_order = configs["resources.#{model[:name]}.columns.order"]
16
+ associations_order = configs["resources.#{model[:name]}.associations.order"]
17
+ actions_order = configs["resources.#{model[:name]}.actions.order"]
18
+ tabs_order = configs["resources.#{model[:name]}.tabs.order"]
19
+
20
+ model.merge(
21
+ columns: sort_by_name(model[:columns], columns_order, sort_alphabetically: false),
22
+ associations: sort_by_name(model[:associations], associations_order),
23
+ actions: sort_by_name(model[:actions], actions_order, sort_alphabetically: false),
24
+ tabs: sort_by_name(model[:tabs], tabs_order, sort_alphabetically: false)
25
+ )
26
+ end
27
+ end
28
+
29
+ # @param list [Array<HashWithIndifferentAccess>]
30
+ # @param sort_alphabetically [Boolean]
31
+ # @param order [Array<String>]
32
+ # @return [Array<HashWithIndifferentAccess>]
33
+ def sort_by_name(list, order, sort_alphabetically: true)
34
+ return list if order.blank? && !sort_alphabetically
35
+
36
+ list.sort_by do |item|
37
+ if order.present?
38
+ order.index(item[:name]) || Float::MAX
39
+ else
40
+ item[:display_name]
41
+ end
42
+ end
43
+ end
44
+
45
+ # @return [Hash<String, HashWithIndifferentAccess>]
46
+ def load_configs
47
+ Motor::Config.all.each_with_object({}) do |config, acc|
48
+ acc[config.key] = config.value
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module Utils
6
+ module_function
7
+
8
+ def slugify(model)
9
+ model.name.underscore.pluralize.gsub('/', '__')
10
+ end
11
+
12
+ def classify_slug(slug)
13
+ slug.singularize.gsub('__', '/').classify.constantize
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './dashboards/persistance'
4
+
5
+ module Motor
6
+ module Dashboards
7
+ end
8
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Dashboards
5
+ module Persistance
6
+ TitleAlreadyExists = Class.new(StandardError)
7
+
8
+ module_function
9
+
10
+ def build_from_params(params, current_user = nil)
11
+ dashboard = assign_attributes(Dashboard.new, params)
12
+
13
+ dashboard.author = current_user
14
+
15
+ dashboard
16
+ end
17
+
18
+ def create_from_params!(params, current_user = nil)
19
+ raise TitleAlreadyExists if Dashboard.exists?(['lower(title) = ?', params[:title].to_s.downcase])
20
+
21
+ dashboard = build_from_params(params, current_user)
22
+
23
+ ApplicationRecord.transaction do
24
+ dashboard.save!
25
+ end
26
+
27
+ dashboard
28
+ rescue ActiveRecord::RecordNotUnique
29
+ retry
30
+ end
31
+
32
+ def update_from_params!(dashboard, params)
33
+ raise TitleAlreadyExists if title_already_exists?(dashboard)
34
+
35
+ dashboard = assign_attributes(dashboard, params)
36
+
37
+ ApplicationRecord.transaction do
38
+ dashboard.save!
39
+ end
40
+
41
+ dashboard.tags.reload
42
+
43
+ dashboard
44
+ rescue ActiveRecord::RecordNotUnique
45
+ retry
46
+ end
47
+
48
+ def assign_attributes(dashboard, params)
49
+ dashboard.assign_attributes(params.slice(:title, :description, :preferences))
50
+
51
+ Motor::Tags.assign_tags(dashboard, params[:tags])
52
+ end
53
+
54
+ def title_already_exists?(dashboard)
55
+ if dashboard.new_record?
56
+ Dashboard.exists?(['lower(title) = ?', dashboard.title.to_s.downcase])
57
+ else
58
+ Dashboard.exists?(['lower(title) = ? AND id != ?', dashboard.title.to_s.downcase, dashboard.id])
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './forms/persistance'
4
+
5
+ module Motor
6
+ module Forms
7
+ end
8
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Forms
5
+ module Persistance
6
+ NameAlreadyExists = Class.new(StandardError)
7
+
8
+ module_function
9
+
10
+ def build_from_params(params, current_user = nil)
11
+ form = assign_attributes(Form.new, params)
12
+
13
+ form.author = current_user
14
+
15
+ form
16
+ end
17
+
18
+ def create_from_params!(params, current_user = nil)
19
+ raise NameAlreadyExists if Form.exists?(['lower(name) = ?', params[:name].to_s.downcase])
20
+
21
+ form = build_from_params(params, current_user)
22
+
23
+ ApplicationRecord.transaction do
24
+ form.save!
25
+ end
26
+
27
+ form
28
+ rescue ActiveRecord::RecordNotUnique
29
+ retry
30
+ end
31
+
32
+ def update_from_params!(form, params)
33
+ raise NameAlreadyExists if name_already_exists?(form)
34
+
35
+ form = assign_attributes(form, params)
36
+
37
+ ApplicationRecord.transaction do
38
+ form.save!
39
+ end
40
+
41
+ form.tags.reload
42
+
43
+ form
44
+ rescue ActiveRecord::RecordNotUnique
45
+ retry
46
+ end
47
+
48
+ def assign_attributes(form, params)
49
+ form.assign_attributes(params.slice(:name, :description, :api_path, :http_method, :preferences))
50
+
51
+ Motor::Tags.assign_tags(form, params[:tags])
52
+ end
53
+
54
+ def name_already_exists?(form)
55
+ if form.new_record?
56
+ Form.exists?(['lower(name) = ?', form.name.to_s.downcase])
57
+ else
58
+ Form.exists?(['lower(name) = ? AND id != ?', form.name.to_s.downcase, form.id])
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ class HashSerializer
5
+ def self.dump(hash)
6
+ hash.to_json
7
+ end
8
+
9
+ def self.load(hash)
10
+ return hash unless hash
11
+
12
+ hash = JSON.parse(hash.presence || '{}') if hash.is_a?(String)
13
+
14
+ if hash.is_a?(Hash)
15
+ hash.with_indifferent_access
16
+ else
17
+ hash || {}
18
+ end
19
+ end
20
+ end
21
+ end