rubymonolith 0.1.5 → 0.1.7
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 +4 -4
- data/README.md +30 -4
- data/Rakefile +2 -0
- data/app/assets/images/monolith/logo.svg +6 -0
- data/app/assets/stylesheets/monolith/application.tailwind.css +14 -0
- data/app/components/monolith/base.rb +6 -0
- data/app/components/monolith/table.rb +37 -0
- data/app/controllers/monolith/application_controller.rb +68 -2
- data/app/controllers/monolith/emails_controller.rb +61 -0
- data/app/controllers/monolith/exceptions_controller.rb +370 -0
- data/app/controllers/monolith/gems_controller.rb +230 -0
- data/app/controllers/monolith/generators_controller.rb +377 -0
- data/app/controllers/monolith/home_controller.rb +10 -0
- data/app/controllers/monolith/models_controller.rb +319 -0
- data/app/controllers/monolith/routes_controller.rb +157 -0
- data/app/controllers/monolith/tables_controller.rb +168 -0
- data/app/views/monolith/base.rb +3 -0
- data/app/views/monolith/layouts/base.rb +23 -0
- data/config/routes.rb +14 -0
- data/lib/generators/monolith/content/USAGE +8 -0
- data/lib/generators/monolith/content/content_generator.rb +25 -0
- data/lib/generators/monolith/generators/base.rb +38 -0
- data/lib/generators/monolith/install/install_generator.rb +14 -95
- data/lib/generators/monolith/view/USAGE +8 -0
- data/lib/generators/monolith/view/view_generator.rb +20 -0
- data/lib/monolith/cli/template.rb +78 -5
- data/lib/monolith/cli.rb +22 -3
- data/lib/monolith/engine.rb +31 -0
- data/lib/monolith/version.rb +1 -1
- data/lib/tasks/monolith_tasks.rake +13 -4
- metadata +66 -10
- data/app/assets/stylesheets/monolith/application.css +0 -15
- data/app/views/layouts/monolith/application.html.erb +0 -15
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# app/controllers/monolith/models_controller.rb
|
|
2
|
+
module Monolith
|
|
3
|
+
class ModelsController < Monolith::ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
render Index.new.tap { _1.models = Model.all }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
model = Model.find(params[:id].to_s)
|
|
10
|
+
return render plain: "Model not found", status: :not_found unless model
|
|
11
|
+
|
|
12
|
+
render Show.new.tap { |v| v.model = model }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# =======================
|
|
16
|
+
# Inline ActiveModel-like object
|
|
17
|
+
# =======================
|
|
18
|
+
class Model
|
|
19
|
+
attr_reader :name, :klass
|
|
20
|
+
|
|
21
|
+
def self.all
|
|
22
|
+
Rails.application.eager_load!
|
|
23
|
+
|
|
24
|
+
models = ActiveRecord::Base.descendants
|
|
25
|
+
.reject(&:abstract_class?)
|
|
26
|
+
.sort_by(&:name)
|
|
27
|
+
|
|
28
|
+
models.map { |klass| new(klass) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.find(name)
|
|
32
|
+
Rails.application.eager_load!
|
|
33
|
+
|
|
34
|
+
klass = ActiveRecord::Base.descendants.find { |k| k.name == name }
|
|
35
|
+
klass && new(klass)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(klass)
|
|
39
|
+
@klass = klass
|
|
40
|
+
@name = klass.name
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_param
|
|
44
|
+
name
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def table_name
|
|
48
|
+
klass.table_name
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def column_names
|
|
52
|
+
klass.column_names
|
|
53
|
+
rescue
|
|
54
|
+
[]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def primary_key
|
|
58
|
+
klass.primary_key
|
|
59
|
+
rescue
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validations
|
|
64
|
+
klass.validators.map do |validator|
|
|
65
|
+
{
|
|
66
|
+
type: validator.class.name.demodulize.gsub(/Validator$/, ''),
|
|
67
|
+
attributes: validator.attributes,
|
|
68
|
+
options: validator.options
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
rescue
|
|
72
|
+
[]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def associations
|
|
76
|
+
klass.reflect_on_all_associations.map do |assoc|
|
|
77
|
+
{
|
|
78
|
+
name: assoc.name,
|
|
79
|
+
type: assoc.macro,
|
|
80
|
+
class_name: assoc.class_name,
|
|
81
|
+
foreign_key: assoc.foreign_key,
|
|
82
|
+
options: assoc.options
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
rescue
|
|
86
|
+
[]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def scopes
|
|
90
|
+
# Get custom scopes (not default scopes)
|
|
91
|
+
scope_names = klass.respond_to?(:scopes) ? klass.scopes.keys : []
|
|
92
|
+
scope_names.map(&:to_s).sort
|
|
93
|
+
rescue
|
|
94
|
+
[]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def callbacks
|
|
98
|
+
[:before_validation, :after_validation, :before_save, :after_save,
|
|
99
|
+
:before_create, :after_create, :before_update, :after_update,
|
|
100
|
+
:before_destroy, :after_destroy].flat_map do |callback_type|
|
|
101
|
+
klass._get_callbacks(callback_type).map do |callback|
|
|
102
|
+
{
|
|
103
|
+
type: callback_type,
|
|
104
|
+
filter: callback.filter
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end.compact
|
|
108
|
+
rescue
|
|
109
|
+
[]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def source_location
|
|
113
|
+
return nil unless klass.respond_to?(:instance_methods)
|
|
114
|
+
|
|
115
|
+
# Try to find the model file
|
|
116
|
+
file_path = "#{Rails.root}/app/models/#{name.underscore}.rb"
|
|
117
|
+
File.exist?(file_path) ? file_path : nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def source_code
|
|
121
|
+
return nil unless source_location
|
|
122
|
+
|
|
123
|
+
File.read(source_location)
|
|
124
|
+
rescue
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def instance_methods
|
|
129
|
+
(klass.instance_methods - ActiveRecord::Base.instance_methods)
|
|
130
|
+
.sort
|
|
131
|
+
.map(&:to_s)
|
|
132
|
+
rescue
|
|
133
|
+
[]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def class_methods
|
|
137
|
+
(klass.methods - ActiveRecord::Base.methods)
|
|
138
|
+
.sort
|
|
139
|
+
.map(&:to_s)
|
|
140
|
+
rescue
|
|
141
|
+
[]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# =======================
|
|
146
|
+
# Phlex views
|
|
147
|
+
# =======================
|
|
148
|
+
class Index < View
|
|
149
|
+
attr_writer :models
|
|
150
|
+
|
|
151
|
+
def view_template
|
|
152
|
+
div(class: "p-6 space-y-4") do
|
|
153
|
+
h1(class: "text-2xl font-bold") { "Models" }
|
|
154
|
+
p(class: "text-sm") { "#{@models.size} ActiveRecord models in your application." }
|
|
155
|
+
|
|
156
|
+
Table @models do
|
|
157
|
+
it.row("Model") {
|
|
158
|
+
nav_link it.name, controller: "/monolith/models", action: :show, id: it.to_param
|
|
159
|
+
}
|
|
160
|
+
it.row("Table") { |m|
|
|
161
|
+
code { m.table_name }
|
|
162
|
+
}
|
|
163
|
+
it.row("Columns") {
|
|
164
|
+
it.column_names.size.to_s
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
class Show < View
|
|
172
|
+
attr_writer :model
|
|
173
|
+
|
|
174
|
+
def view_template
|
|
175
|
+
m = @model
|
|
176
|
+
|
|
177
|
+
div(class: "p-6 space-y-6") do
|
|
178
|
+
h1(class: "text-2xl font-bold") { m.name }
|
|
179
|
+
p { code { m.table_name } }
|
|
180
|
+
|
|
181
|
+
# Columns
|
|
182
|
+
section do
|
|
183
|
+
h2(class: "text-xl font-bold mb-2") { "Columns (#{m.column_names.size})" }
|
|
184
|
+
if m.column_names.any?
|
|
185
|
+
ul(class: "list-disc pl-6") do
|
|
186
|
+
m.column_names.each do |col|
|
|
187
|
+
li {
|
|
188
|
+
plain col
|
|
189
|
+
if col == m.primary_key
|
|
190
|
+
span(class: "text-sm") { " (primary key)" }
|
|
191
|
+
end
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
else
|
|
196
|
+
p { em { "No columns" } }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Validations
|
|
201
|
+
section do
|
|
202
|
+
h2(class: "text-xl font-bold mb-2") { "Validations (#{m.validations.size})" }
|
|
203
|
+
if m.validations.any?
|
|
204
|
+
ul(class: "list-disc pl-6 space-y-1") do
|
|
205
|
+
m.validations.each do |validation|
|
|
206
|
+
li do
|
|
207
|
+
strong { validation[:type] }
|
|
208
|
+
plain " on "
|
|
209
|
+
code { validation[:attributes].join(", ") }
|
|
210
|
+
if validation[:options].any?
|
|
211
|
+
plain " "
|
|
212
|
+
code { validation[:options].inspect }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
else
|
|
218
|
+
p { em { "No validations" } }
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Associations
|
|
223
|
+
section do
|
|
224
|
+
h2(class: "text-xl font-bold mb-2") { "Associations (#{m.associations.size})" }
|
|
225
|
+
if m.associations.any?
|
|
226
|
+
ul(class: "list-disc pl-6 space-y-1") do
|
|
227
|
+
m.associations.each do |assoc|
|
|
228
|
+
li do
|
|
229
|
+
strong { assoc[:type].to_s }
|
|
230
|
+
plain " :"
|
|
231
|
+
code { assoc[:name].to_s }
|
|
232
|
+
plain " → "
|
|
233
|
+
plain assoc[:class_name]
|
|
234
|
+
plain " (FK: "
|
|
235
|
+
code { assoc[:foreign_key].to_s }
|
|
236
|
+
plain ")"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
else
|
|
241
|
+
p { em { "No associations" } }
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Scopes
|
|
246
|
+
if m.scopes.any?
|
|
247
|
+
section do
|
|
248
|
+
h2(class: "text-xl font-bold mb-2") { "Scopes (#{m.scopes.size})" }
|
|
249
|
+
ul(class: "list-disc pl-6") do
|
|
250
|
+
m.scopes.each do |scope_name|
|
|
251
|
+
li { code { scope_name } }
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Callbacks
|
|
258
|
+
if m.callbacks.any?
|
|
259
|
+
section do
|
|
260
|
+
h2(class: "text-xl font-bold mb-2") { "Callbacks (#{m.callbacks.size})" }
|
|
261
|
+
ul(class: "list-disc pl-6 space-y-1") do
|
|
262
|
+
m.callbacks.each do |callback|
|
|
263
|
+
li do
|
|
264
|
+
strong { callback[:type].to_s }
|
|
265
|
+
plain " → "
|
|
266
|
+
code { callback[:filter].to_s }
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Instance Methods
|
|
274
|
+
if m.instance_methods.any?
|
|
275
|
+
section do
|
|
276
|
+
h2(class: "text-xl font-bold mb-2") { "Instance Methods (#{m.instance_methods.size})" }
|
|
277
|
+
details do
|
|
278
|
+
summary(class: "cursor-pointer") { "Show methods" }
|
|
279
|
+
ul(class: "list-disc pl-6 mt-2 grid grid-cols-2 md:grid-cols-3 gap-1") do
|
|
280
|
+
m.instance_methods.each do |method_name|
|
|
281
|
+
li { code { method_name } }
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Class Methods
|
|
289
|
+
if m.class_methods.any?
|
|
290
|
+
section do
|
|
291
|
+
h2(class: "text-xl font-bold mb-2") { "Class Methods (#{m.class_methods.size})" }
|
|
292
|
+
details do
|
|
293
|
+
summary(class: "cursor-pointer") { "Show methods" }
|
|
294
|
+
ul(class: "list-disc pl-6 mt-2 grid grid-cols-2 md:grid-cols-3 gap-1") do
|
|
295
|
+
m.class_methods.each do |method_name|
|
|
296
|
+
li { code { method_name } }
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Source Code
|
|
304
|
+
if m.source_code
|
|
305
|
+
section do
|
|
306
|
+
h2(class: "text-xl font-bold mb-2") { "Source Code" }
|
|
307
|
+
p(class: "text-sm mb-2") { code { m.source_location } }
|
|
308
|
+
pre(class: "border p-4 overflow-x-auto text-xs") do
|
|
309
|
+
code { m.source_code }
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
div(class: "pt-4") { nav_link "← All models", controller: "/monolith/models", action: :index }
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# app/controllers/monolith/routes_controller.rb
|
|
2
|
+
module Monolith
|
|
3
|
+
class RoutesController < Monolith::ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
render Index.new.tap { _1.routes = Route.all }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
route = Route.find(params[:id].to_s)
|
|
10
|
+
return render plain: "Route not found", status: :not_found unless route
|
|
11
|
+
|
|
12
|
+
render Show.new.tap { |v| v.route = route }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# =======================
|
|
16
|
+
# Inline ActiveModel-like object
|
|
17
|
+
# =======================
|
|
18
|
+
class Route
|
|
19
|
+
attr_reader :name, :verb, :path, :controller, :action, :constraints, :defaults, :required_parts
|
|
20
|
+
|
|
21
|
+
def self.all
|
|
22
|
+
Rails.application.routes.routes.map.with_index { |route, idx| from_route(route, idx) }.compact.sort_by(&:display_name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.find(id)
|
|
26
|
+
all.find { |r| r.to_param == id }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.from_route(route, idx)
|
|
30
|
+
name = route.name.to_s
|
|
31
|
+
verb = route.verb.to_s
|
|
32
|
+
path = route.path.spec.to_s
|
|
33
|
+
|
|
34
|
+
# Extract controller and action
|
|
35
|
+
defaults = route.defaults
|
|
36
|
+
controller = defaults[:controller]
|
|
37
|
+
action = defaults[:action]
|
|
38
|
+
|
|
39
|
+
# Skip routes without controller/action or internal Rails routes
|
|
40
|
+
return nil if controller.nil? || action.nil?
|
|
41
|
+
return nil if controller.to_s.start_with?("rails/")
|
|
42
|
+
|
|
43
|
+
new(
|
|
44
|
+
name: name.empty? ? nil : name,
|
|
45
|
+
verb: verb,
|
|
46
|
+
path: path,
|
|
47
|
+
controller: controller,
|
|
48
|
+
action: action,
|
|
49
|
+
constraints: route.constraints.except(:request_method),
|
|
50
|
+
defaults: defaults.except(:controller, :action),
|
|
51
|
+
required_parts: route.required_parts,
|
|
52
|
+
id: idx
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(name:, verb:, path:, controller:, action:, constraints:, defaults:, required_parts:, id:)
|
|
57
|
+
@name = name
|
|
58
|
+
@verb = verb
|
|
59
|
+
@path = path
|
|
60
|
+
@controller = controller
|
|
61
|
+
@action = action
|
|
62
|
+
@constraints = constraints
|
|
63
|
+
@defaults = defaults
|
|
64
|
+
@required_parts = required_parts
|
|
65
|
+
@id = id
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_param
|
|
69
|
+
@id.to_s
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def display_name
|
|
73
|
+
name || "#{controller}##{action}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def full_controller_action
|
|
77
|
+
"#{controller}##{action}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# =======================
|
|
82
|
+
# Phlex views
|
|
83
|
+
# =======================
|
|
84
|
+
class Index < View
|
|
85
|
+
attr_writer :routes
|
|
86
|
+
|
|
87
|
+
def view_template
|
|
88
|
+
div(class: "p-6 space-y-4") do
|
|
89
|
+
h1(class: "text-2xl font-bold") { "Routes" }
|
|
90
|
+
p(class: "text-sm") { "#{@routes.size} routes in your Rails application." }
|
|
91
|
+
|
|
92
|
+
Table @routes do
|
|
93
|
+
it.row("Name") {
|
|
94
|
+
nav_link it.display_name, controller: "/monolith/routes", action: :show, id: it.to_param
|
|
95
|
+
}
|
|
96
|
+
it.row("Verb") { |r|
|
|
97
|
+
code { r.verb }
|
|
98
|
+
}
|
|
99
|
+
it.row("Path") { |r|
|
|
100
|
+
code { r.path }
|
|
101
|
+
}
|
|
102
|
+
it.row("Controller#Action") {
|
|
103
|
+
it.full_controller_action
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class Show < View
|
|
111
|
+
attr_writer :route
|
|
112
|
+
|
|
113
|
+
def view_template
|
|
114
|
+
r = @route
|
|
115
|
+
|
|
116
|
+
div(class: "p-6 space-y-4") do
|
|
117
|
+
h1(class: "text-2xl font-bold") { r.display_name }
|
|
118
|
+
p { code { "#{r.verb} #{r.path}" } }
|
|
119
|
+
|
|
120
|
+
dl(class: "grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-2") do
|
|
121
|
+
dt(class: "font-semibold") { "Name" }
|
|
122
|
+
dd { r.name || em { "—" } }
|
|
123
|
+
|
|
124
|
+
dt(class: "font-semibold") { "Verb" }
|
|
125
|
+
dd { code { r.verb } }
|
|
126
|
+
|
|
127
|
+
dt(class: "font-semibold") { "Path" }
|
|
128
|
+
dd { code { r.path } }
|
|
129
|
+
|
|
130
|
+
dt(class: "font-semibold") { "Controller" }
|
|
131
|
+
dd { r.controller }
|
|
132
|
+
|
|
133
|
+
dt(class: "font-semibold") { "Action" }
|
|
134
|
+
dd { r.action }
|
|
135
|
+
|
|
136
|
+
if r.required_parts.any?
|
|
137
|
+
dt(class: "font-semibold") { "Required Parts" }
|
|
138
|
+
dd { r.required_parts.join(", ") }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if r.defaults.any?
|
|
142
|
+
dt(class: "font-semibold") { "Defaults" }
|
|
143
|
+
dd { code { r.defaults.inspect } }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if r.constraints.any?
|
|
147
|
+
dt(class: "font-semibold") { "Constraints" }
|
|
148
|
+
dd { code { r.constraints.inspect } }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
div(class: "pt-4") { nav_link "← All routes", controller: "/monolith/routes", action: :index }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# app/controllers/monolith/tables_controller.rb
|
|
2
|
+
module Monolith
|
|
3
|
+
class TablesController < Monolith::ApplicationController
|
|
4
|
+
before_action :set_pagination
|
|
5
|
+
before_action :set_table, only: :show
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
render Index.new.tap { _1.tables = Table.all }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
not_found! unless @table
|
|
13
|
+
rows = @table.rows(page: @page, per_page: @per_page)
|
|
14
|
+
render Show.new.tap { |v|
|
|
15
|
+
v.table = @table
|
|
16
|
+
v.rows = rows[:data]
|
|
17
|
+
v.total = rows[:total]
|
|
18
|
+
v.page = @page
|
|
19
|
+
v.per_page= @per_page
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def set_pagination
|
|
26
|
+
@page = params[:page].to_i
|
|
27
|
+
@page = 1 if @page < 1
|
|
28
|
+
@per_page = (params[:per_page] || 50).to_i
|
|
29
|
+
@per_page = 50 if @per_page <= 0
|
|
30
|
+
@per_page = 500 if @per_page > 500
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def set_table
|
|
34
|
+
@table = Table.find(params[:id].to_s) if params[:id].present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def not_found!
|
|
38
|
+
render plain: "Not found", status: :not_found
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# =======================
|
|
42
|
+
# Inline ActiveModel-like object
|
|
43
|
+
# =======================
|
|
44
|
+
class Table
|
|
45
|
+
attr_reader :name
|
|
46
|
+
|
|
47
|
+
EXCLUDED = %w[schema_migrations ar_internal_metadata].freeze
|
|
48
|
+
|
|
49
|
+
def self.all
|
|
50
|
+
tables.map { |t| new(t) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.find(name)
|
|
54
|
+
return nil unless tables.include?(name)
|
|
55
|
+
new(name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ---- instance ----
|
|
59
|
+
def initialize(name)
|
|
60
|
+
@name = name
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_param = name
|
|
64
|
+
|
|
65
|
+
def columns
|
|
66
|
+
@columns ||= conn.columns(name).map(&:name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def primary_key
|
|
70
|
+
@primary_key ||= conn.primary_key(name)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns { data: [Hash], total: Integer }
|
|
74
|
+
def rows(page:, per_page:)
|
|
75
|
+
offset = (page - 1) * per_page
|
|
76
|
+
total = conn.exec_query("SELECT COUNT(*) AS c FROM #{qtn(name)}").first["c"]
|
|
77
|
+
|
|
78
|
+
order_sql = primary_key ? "ORDER BY #{qcn(primary_key)}" : ""
|
|
79
|
+
sql = <<~SQL
|
|
80
|
+
SELECT *
|
|
81
|
+
FROM #{qtn(name)}
|
|
82
|
+
#{order_sql}
|
|
83
|
+
LIMIT #{per_page.to_i} OFFSET #{offset.to_i}
|
|
84
|
+
SQL
|
|
85
|
+
|
|
86
|
+
data = conn.exec_query(sql).to_a
|
|
87
|
+
{ data: data, total: total }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def self.conn = ActiveRecord::Base.connection
|
|
93
|
+
def conn = self.class.conn
|
|
94
|
+
|
|
95
|
+
def self.tables
|
|
96
|
+
@tables ||= (conn.tables - EXCLUDED).sort
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def qtn(t) = conn.quote_table_name(t)
|
|
100
|
+
def qcn(c) = "#{qtn(name)}.#{conn.quote_column_name(c)}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# =======================
|
|
104
|
+
# Phlex views
|
|
105
|
+
# =======================
|
|
106
|
+
class Index < View
|
|
107
|
+
attr_writer :tables
|
|
108
|
+
|
|
109
|
+
def view_template
|
|
110
|
+
div(class: "p-6 space-y-4") do
|
|
111
|
+
h1(class: "text-2xl font-bold") { "Tables" }
|
|
112
|
+
ul(class: "list-disc pl-6 space-y-1") do
|
|
113
|
+
@tables.each do |t|
|
|
114
|
+
li { nav_link t.name, controller: "/monolith/tables", action: :show, id: t.name }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class Show < View
|
|
122
|
+
attr_writer :table, :rows, :page, :per_page, :total
|
|
123
|
+
|
|
124
|
+
def view_template
|
|
125
|
+
div(class: "p-6 space-y-4") do
|
|
126
|
+
h1(class: "text-2xl font-bold") { @table.name }
|
|
127
|
+
p do
|
|
128
|
+
plain "Rows "
|
|
129
|
+
strong { "#{start_row}-#{end_row}" }
|
|
130
|
+
plain " of #{@total} (per page: #{@per_page})"
|
|
131
|
+
if @table.primary_key
|
|
132
|
+
span(class: "ml-2 text-sm") { "PK: #{@table.primary_key}" }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Table @table.columns do
|
|
137
|
+
it.row(col) {
|
|
138
|
+
format_cell(it[col])
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
div(class: "flex items-center gap-3") do
|
|
143
|
+
if @page > 1
|
|
144
|
+
nav_link "← Prev", controller: "/monolith/tables", action: :show, id: @table.name, page: @page - 1, per_page: @per_page
|
|
145
|
+
end
|
|
146
|
+
if end_row < @total
|
|
147
|
+
nav_link "Next →", controller: "/monolith/tables", action: :show, id: @table.name, page: @page + 1, per_page: @per_page
|
|
148
|
+
end
|
|
149
|
+
span(class: "text-sm ml-auto") { "Page #{@page}" }
|
|
150
|
+
nav_link "All tables", controller: "/monolith/tables", action: :index
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def start_row = ((@page - 1) * @per_page) + 1
|
|
156
|
+
def end_row = [@page * @per_page, @total].min
|
|
157
|
+
|
|
158
|
+
def format_cell(v)
|
|
159
|
+
case v
|
|
160
|
+
when Hash, Array then code { v.ai(plain: true) rescue v.to_json }
|
|
161
|
+
when Time, DateTime, Date then v.iso8601
|
|
162
|
+
else
|
|
163
|
+
v.nil? ? em { "NULL" } : v.to_s
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class Monolith::Views::Layouts::Base < Monolith::Views::Base
|
|
2
|
+
def around_template(&)
|
|
3
|
+
html do
|
|
4
|
+
head do
|
|
5
|
+
title { @title }
|
|
6
|
+
meta name: "viewport", content: "width=device-width,initial-scale=1"
|
|
7
|
+
meta charset: "utf-8"
|
|
8
|
+
meta name: "apple-mobile-web-app-capable", content: "yes"
|
|
9
|
+
meta name: "apple-mobile-web-app-status-bar-style", content: "black-translucent"
|
|
10
|
+
csp_meta_tag
|
|
11
|
+
csrf_meta_tags
|
|
12
|
+
stylesheet_link_tag "monolith/tailwind", data_turbo_track: "reload"
|
|
13
|
+
javascript_importmap_tags
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
body(&)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def view_template
|
|
21
|
+
yield self if block_given?
|
|
22
|
+
end
|
|
23
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
Monolith::Engine.routes.draw do
|
|
2
|
+
root to: "home#show"
|
|
3
|
+
|
|
4
|
+
resources :emails, only: [:index, :show]
|
|
5
|
+
resources :tables, only: [:index, :show]
|
|
6
|
+
resources :gems, only: [:index, :show]
|
|
7
|
+
resources :routes, only: [:index, :show]
|
|
8
|
+
resources :models, only: [:index, :show]
|
|
9
|
+
resources :generators, only: [:index, :show] do
|
|
10
|
+
member do
|
|
11
|
+
post :create
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
get "/exceptions/:id", to: "exceptions#show", as: :exception
|
|
2
16
|
end
|