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,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