dbviewer 0.6.2 → 0.6.4
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 +43 -14
- data/app/controllers/concerns/dbviewer/connection_management.rb +88 -0
- data/app/controllers/concerns/dbviewer/data_export.rb +32 -0
- data/app/controllers/concerns/dbviewer/database_information.rb +62 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +8 -514
- data/app/controllers/concerns/dbviewer/datatable_support.rb +47 -0
- data/app/controllers/concerns/dbviewer/query_operations.rb +28 -0
- data/app/controllers/concerns/dbviewer/relationship_management.rb +173 -0
- data/app/controllers/concerns/dbviewer/table_operations.rb +56 -0
- data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +26 -24
- data/app/controllers/dbviewer/tables_controller.rb +16 -11
- data/app/helpers/dbviewer/application_helper.rb +9 -521
- data/app/helpers/dbviewer/database_helper.rb +59 -0
- data/app/helpers/dbviewer/filter_helper.rb +137 -0
- data/app/helpers/dbviewer/formatting_helper.rb +30 -0
- data/app/helpers/dbviewer/navigation_helper.rb +35 -0
- data/app/helpers/dbviewer/pagination_helper.rb +72 -0
- data/app/helpers/dbviewer/sorting_helper.rb +47 -0
- data/app/helpers/dbviewer/table_rendering_helper.rb +145 -0
- data/app/helpers/dbviewer/ui_helper.rb +41 -0
- data/app/views/dbviewer/tables/show.html.erb +225 -139
- data/app/views/layouts/dbviewer/application.html.erb +55 -0
- data/lib/dbviewer/database/dynamic_model_factory.rb +40 -5
- data/lib/dbviewer/database/manager.rb +2 -3
- data/lib/dbviewer/datatable/query_operations.rb +84 -214
- data/lib/dbviewer/engine.rb +1 -22
- data/lib/dbviewer/query/executor.rb +1 -36
- data/lib/dbviewer/query/notification_subscriber.rb +46 -0
- data/lib/dbviewer/validator/sql.rb +198 -0
- data/lib/dbviewer/validator.rb +9 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +69 -45
- data/lib/generators/dbviewer/templates/initializer.rb +15 -0
- metadata +20 -3
- data/lib/dbviewer/sql_validator.rb +0 -194
@@ -1,525 +1,13 @@
|
|
1
1
|
module Dbviewer
|
2
2
|
module ApplicationHelper
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
# Helper to access the database manager
|
13
|
-
def get_database_manager
|
14
|
-
@database_manager ||= ::Dbviewer::Database::Manager.new
|
15
|
-
end
|
16
|
-
|
17
|
-
# Extract column type from columns info
|
18
|
-
def column_type_from_info(column_name, columns)
|
19
|
-
return nil unless columns.present?
|
20
|
-
|
21
|
-
column_info = columns.find { |c| c[:name].to_s == column_name.to_s }
|
22
|
-
column_info ? column_info[:type].to_s.downcase : nil
|
23
|
-
end
|
24
|
-
|
25
|
-
# Determine default operator based on column type
|
26
|
-
def default_operator_for_column_type(column_type)
|
27
|
-
if column_type && column_type =~ /char|text|string|uuid|enum/i
|
28
|
-
"contains"
|
29
|
-
else
|
30
|
-
"eq"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# Generate operator options based on column type
|
35
|
-
def operator_options_for_column_type(column_type)
|
36
|
-
# Common operators for all types
|
37
|
-
common_operators = [
|
38
|
-
[ "is null", "is_null" ],
|
39
|
-
[ "is not null", "is_not_null" ]
|
40
|
-
]
|
41
|
-
|
42
|
-
type_specific_operators = if column_type && (column_type =~ /datetime/ || column_type =~ /^date$/ || column_type =~ /^time$/)
|
43
|
-
# Date/Time operators
|
44
|
-
[
|
45
|
-
[ "=", "eq" ],
|
46
|
-
[ "≠", "neq" ],
|
47
|
-
[ "<", "lt" ],
|
48
|
-
[ ">", "gt" ],
|
49
|
-
[ "≤", "lte" ],
|
50
|
-
[ "≥", "gte" ]
|
51
|
-
]
|
52
|
-
elsif column_type && column_type =~ /int|float|decimal|double|number|numeric|real|money|bigint|smallint|tinyint|mediumint|bit/i
|
53
|
-
# Numeric operators
|
54
|
-
[
|
55
|
-
[ "=", "eq" ],
|
56
|
-
[ "≠", "neq" ],
|
57
|
-
[ "<", "lt" ],
|
58
|
-
[ ">", "gt" ],
|
59
|
-
[ "≤", "lte" ],
|
60
|
-
[ "≥", "gte" ]
|
61
|
-
]
|
62
|
-
else
|
63
|
-
# Text operators
|
64
|
-
[
|
65
|
-
[ "contains", "contains" ],
|
66
|
-
[ "not contains", "not_contains" ],
|
67
|
-
[ "=", "eq" ],
|
68
|
-
[ "≠", "neq" ],
|
69
|
-
[ "starts with", "starts_with" ],
|
70
|
-
[ "ends with", "ends_with" ]
|
71
|
-
]
|
72
|
-
end
|
73
|
-
|
74
|
-
# Return type-specific operators first, then common operators
|
75
|
-
type_specific_operators + common_operators
|
76
|
-
end
|
77
|
-
|
78
|
-
# Render column filter input based on column type
|
79
|
-
def render_column_filter_input(form, column_name, column_type, column_filters)
|
80
|
-
# Get selected operator to check if it's a null operator
|
81
|
-
operator = column_filters["#{column_name}_operator"]
|
82
|
-
is_null_operator = operator == "is_null" || operator == "is_not_null"
|
83
|
-
|
84
|
-
# Clean up the value for non-null operators if the value contains a null operator
|
85
|
-
# This ensures we don't carry over 'is_null' or 'is_not_null' values when switching operators
|
86
|
-
value = column_filters[column_name]
|
87
|
-
if !is_null_operator && value.present? && (value == "is_null" || value == "is_not_null")
|
88
|
-
value = nil
|
89
|
-
end
|
90
|
-
|
91
|
-
# For null operators, display a non-editable field without placeholder
|
92
|
-
if is_null_operator
|
93
|
-
# Keep a hidden field for the actual value
|
94
|
-
hidden_field = form.hidden_field("column_filters[#{column_name}]",
|
95
|
-
value: operator,
|
96
|
-
class: "null-filter-value",
|
97
|
-
data: { column: column_name })
|
98
|
-
|
99
|
-
# Add a visible but disabled text field with no placeholder or value
|
100
|
-
visible_field = form.text_field("column_filters[#{column_name}_display]",
|
101
|
-
disabled: true,
|
102
|
-
value: "",
|
103
|
-
class: "form-control form-control-sm column-filter rounded-0 disabled-filter",
|
104
|
-
data: { column: "#{column_name}_display" })
|
105
|
-
|
106
|
-
hidden_field + visible_field
|
107
|
-
elsif column_type && column_type =~ /datetime/
|
108
|
-
form.datetime_local_field("column_filters[#{column_name}]",
|
109
|
-
value: value,
|
110
|
-
class: "form-control form-control-sm column-filter rounded-0",
|
111
|
-
data: { column: column_name })
|
112
|
-
elsif column_type && column_type =~ /^date$/
|
113
|
-
form.date_field("column_filters[#{column_name}]",
|
114
|
-
value: value,
|
115
|
-
class: "form-control form-control-sm column-filter rounded-0",
|
116
|
-
data: { column: column_name })
|
117
|
-
elsif column_type && column_type =~ /^time$/
|
118
|
-
form.time_field("column_filters[#{column_name}]",
|
119
|
-
value: value,
|
120
|
-
class: "form-control form-control-sm column-filter rounded-0",
|
121
|
-
data: { column: column_name })
|
122
|
-
else
|
123
|
-
form.text_field("column_filters[#{column_name}]",
|
124
|
-
value: value,
|
125
|
-
placeholder: "",
|
126
|
-
class: "form-control form-control-sm column-filter rounded-0",
|
127
|
-
data: { column: column_name })
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Render operator select for column filter
|
132
|
-
def render_operator_select(form, column_name, column_type, column_filters)
|
133
|
-
# Get previously selected operator or default
|
134
|
-
default_operator = default_operator_for_column_type(column_type)
|
135
|
-
selected_operator = column_filters["#{column_name}_operator"]
|
136
|
-
selected_operator = default_operator if selected_operator.nil? || selected_operator == "default"
|
137
|
-
|
138
|
-
# Get appropriate options
|
139
|
-
operator_options = operator_options_for_column_type(column_type)
|
140
|
-
|
141
|
-
form.select("column_filters[#{column_name}_operator]",
|
142
|
-
options_for_select(operator_options, selected_operator),
|
143
|
-
{ include_blank: false },
|
144
|
-
{ class: "form-select form-select-sm operator-select" })
|
145
|
-
end
|
146
|
-
|
147
|
-
# Render complete filter input group for a column
|
148
|
-
def render_column_filter(form, column_name, columns, column_filters)
|
149
|
-
column_type = column_type_from_info(column_name, columns)
|
150
|
-
|
151
|
-
content_tag(:div, class: "filter-input-group") do
|
152
|
-
operator_select = render_operator_select(form, column_name, column_type, column_filters)
|
153
|
-
input_field = render_column_filter_input(form, column_name, column_type, column_filters)
|
154
|
-
|
155
|
-
operator_select + input_field
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def format_cell_value(value)
|
160
|
-
return "NULL" if value.nil?
|
161
|
-
return value.to_s.truncate(100) unless value.is_a?(String)
|
162
|
-
|
163
|
-
case value
|
164
|
-
when /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
|
165
|
-
# ISO 8601 datetime
|
166
|
-
begin
|
167
|
-
Time.parse(value).strftime("%Y-%m-%d %H:%M:%S")
|
168
|
-
rescue
|
169
|
-
value.to_s.truncate(100)
|
170
|
-
end
|
171
|
-
when /\A\d{4}-\d{2}-\d{2}\z/
|
172
|
-
# Date
|
173
|
-
value
|
174
|
-
when /\A{.+}\z/, /\A\[.+\]\z/
|
175
|
-
# JSON
|
176
|
-
begin
|
177
|
-
JSON.pretty_generate(JSON.parse(value)).truncate(100)
|
178
|
-
rescue
|
179
|
-
value.to_s.truncate(100)
|
180
|
-
end
|
181
|
-
else
|
182
|
-
value.to_s.truncate(100)
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# Common parameters for pagination and filtering
|
187
|
-
def common_params(options = {})
|
188
|
-
params = {
|
189
|
-
order_by: @order_by,
|
190
|
-
order_direction: @order_direction,
|
191
|
-
per_page: @per_page,
|
192
|
-
column_filters: @column_filters
|
193
|
-
}.merge(options)
|
194
|
-
|
195
|
-
# Add creation filters if they exist
|
196
|
-
params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
197
|
-
params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
198
|
-
|
199
|
-
params
|
200
|
-
end
|
201
|
-
|
202
|
-
# Render pagination UI
|
203
|
-
def render_pagination(table_name, current_page, total_pages, params = {})
|
204
|
-
return unless total_pages && total_pages > 1
|
205
|
-
|
206
|
-
content_tag(:nav, 'aria-label': "Page navigation") do
|
207
|
-
content_tag(:ul, class: "pagination justify-content-center") do
|
208
|
-
prev_link = content_tag(:li, class: "page-item #{current_page == 1 ? 'disabled' : ''}") do
|
209
|
-
link_to "«", table_path(table_name, params.merge(page: [ current_page - 1, 1 ].max)), class: "page-link"
|
210
|
-
end
|
211
|
-
|
212
|
-
# Calculate page range to display
|
213
|
-
start_page = [ 1, current_page - 2 ].max
|
214
|
-
end_page = [ start_page + 4, total_pages ].min
|
215
|
-
start_page = [ 1, end_page - 4 ].max
|
216
|
-
|
217
|
-
# Generate page links
|
218
|
-
page_links = (start_page..end_page).map do |page_num|
|
219
|
-
content_tag(:li, class: "page-item #{page_num == current_page ? 'active' : ''}") do
|
220
|
-
link_to page_num, table_path(table_name, params.merge(page: page_num)), class: "page-link"
|
221
|
-
end
|
222
|
-
end.join.html_safe
|
223
|
-
|
224
|
-
next_link = content_tag(:li, class: "page-item #{current_page == total_pages ? 'disabled' : ''}") do
|
225
|
-
link_to "»", table_path(table_name, params.merge(page: [ current_page + 1, total_pages ].min)), class: "page-link"
|
226
|
-
end
|
227
|
-
|
228
|
-
prev_link + page_links + next_link
|
229
|
-
end
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
# Generate URL parameters for per-page dropdown
|
234
|
-
def per_page_url_params(table_name)
|
235
|
-
# Start with the dynamic part for the select element
|
236
|
-
url_params = "per_page=' + this.value + '&page=1"
|
237
|
-
|
238
|
-
# Add all other common parameters except per_page and page which we already set
|
239
|
-
params = common_params.except(:per_page, :page)
|
240
|
-
|
241
|
-
# Convert the params hash to URL parameters
|
242
|
-
params.each do |key, value|
|
243
|
-
if key == :column_filters && value.is_a?(Hash) && value.reject { |_, v| v.blank? }.any?
|
244
|
-
value.reject { |_, v| v.blank? }.each do |filter_key, filter_value|
|
245
|
-
url_params += "&column_filters[#{filter_key}]=#{CGI.escape(filter_value.to_s)}"
|
246
|
-
end
|
247
|
-
elsif value.present?
|
248
|
-
url_params += "&#{key}=#{CGI.escape(value.to_s)}"
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
url_params
|
253
|
-
end
|
254
|
-
|
255
|
-
# Render time grouping links
|
256
|
-
def time_grouping_links(table_name, current_grouping)
|
257
|
-
params = common_params
|
258
|
-
|
259
|
-
content_tag(:div, class: "btn-group btn-group-sm", role: "group", 'aria-label': "Time grouping") do
|
260
|
-
[
|
261
|
-
link_to("Hourly", table_path(table_name, params.merge(time_group: "hourly")),
|
262
|
-
class: "btn btn-outline-primary #{current_grouping == 'hourly' ? 'active' : ''}"),
|
263
|
-
link_to("Daily", table_path(table_name, params.merge(time_group: "daily")),
|
264
|
-
class: "btn btn-outline-primary #{current_grouping == 'daily' ? 'active' : ''}"),
|
265
|
-
link_to("Weekly", table_path(table_name, params.merge(time_group: "weekly")),
|
266
|
-
class: "btn btn-outline-primary #{current_grouping == 'weekly' ? 'active' : ''}")
|
267
|
-
].join.html_safe
|
268
|
-
end
|
269
|
-
end
|
270
|
-
|
271
|
-
# Dark mode helper methods
|
272
|
-
|
273
|
-
# Returns the theme toggle icon based on the current theme
|
274
|
-
def theme_toggle_icon
|
275
|
-
'<i class="bi bi-moon"></i><i class="bi bi-sun"></i>'.html_safe
|
276
|
-
end
|
277
|
-
|
278
|
-
# Returns the aria label for the theme toggle button
|
279
|
-
def theme_toggle_label
|
280
|
-
"Toggle dark mode"
|
281
|
-
end
|
282
|
-
|
283
|
-
# Returns the appropriate background class for stat cards that adapts to dark mode
|
284
|
-
def stat_card_bg_class
|
285
|
-
"stat-card-bg"
|
286
|
-
end
|
287
|
-
|
288
|
-
# Helper method for code blocks background that adapts to dark mode
|
289
|
-
def code_block_bg_class
|
290
|
-
"sql-code-block"
|
291
|
-
end
|
292
|
-
|
293
|
-
# Determine if the current table should be active in the sidebar
|
294
|
-
def current_table?(table_name)
|
295
|
-
@table_name.present? && @table_name == table_name
|
296
|
-
end
|
297
|
-
|
298
|
-
# Format table name for display - truncate if too long
|
299
|
-
def format_table_name(table_name)
|
300
|
-
if table_name.length > 20
|
301
|
-
"#{table_name.first(17)}..."
|
302
|
-
else
|
303
|
-
table_name
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
# Get appropriate icon for column data type
|
308
|
-
def column_type_icon(column_type)
|
309
|
-
case column_type.to_s.downcase
|
310
|
-
when /int/, /serial/, /number/, /decimal/, /float/, /double/
|
311
|
-
"bi-123"
|
312
|
-
when /char/, /text/, /string/, /uuid/
|
313
|
-
"bi-fonts"
|
314
|
-
when /date/, /time/
|
315
|
-
"bi-calendar"
|
316
|
-
when /bool/
|
317
|
-
"bi-toggle-on"
|
318
|
-
when /json/, /jsonb/
|
319
|
-
"bi-braces"
|
320
|
-
when /array/
|
321
|
-
"bi-list-ol"
|
322
|
-
else
|
323
|
-
"bi-file-earmark"
|
324
|
-
end
|
325
|
-
end
|
326
|
-
|
327
|
-
# Helper method to determine if current controller and action match
|
328
|
-
def active_nav_class(controller_name, action_name = nil)
|
329
|
-
current_controller = params[:controller].split("/").last
|
330
|
-
active = current_controller == controller_name
|
331
|
-
|
332
|
-
if action_name.present?
|
333
|
-
active = active && params[:action] == action_name
|
334
|
-
end
|
335
|
-
|
336
|
-
active ? "active" : ""
|
337
|
-
end
|
338
|
-
|
339
|
-
# Helper for highlighting dashboard link
|
340
|
-
def dashboard_nav_class
|
341
|
-
active_nav_class("home")
|
342
|
-
end
|
343
|
-
|
344
|
-
# Helper for highlighting tables link
|
345
|
-
def tables_nav_class
|
346
|
-
active_nav_class("tables")
|
347
|
-
end
|
348
|
-
|
349
|
-
# Helper for highlighting ERD link
|
350
|
-
def erd_nav_class
|
351
|
-
active_nav_class("entity_relationship_diagrams")
|
352
|
-
end
|
353
|
-
|
354
|
-
# Helper for highlighting SQL Logs link
|
355
|
-
def logs_nav_class
|
356
|
-
active_nav_class("logs")
|
357
|
-
end
|
358
|
-
|
359
|
-
# Returns a sort icon based on the current sort direction
|
360
|
-
def sort_icon(column_name, current_order_by, current_direction)
|
361
|
-
if column_name == current_order_by
|
362
|
-
direction = current_direction == "ASC" ? "up" : "down"
|
363
|
-
"<i class='bi bi-sort-#{direction}'></i>".html_safe
|
364
|
-
else
|
365
|
-
"<i class='bi bi-filter invisible sort-icon'></i>".html_safe
|
366
|
-
end
|
367
|
-
end
|
368
|
-
|
369
|
-
# Determine the next sort direction based on the current one
|
370
|
-
def next_sort_direction(column_name, current_order_by, current_direction)
|
371
|
-
if column_name == current_order_by && current_direction == "ASC"
|
372
|
-
"DESC"
|
373
|
-
else
|
374
|
-
"ASC"
|
375
|
-
end
|
376
|
-
end
|
377
|
-
|
378
|
-
# Generate a sortable column header link
|
379
|
-
def sortable_column_header(column_name, current_order_by, current_direction, table_name, current_page, per_page, column_filters)
|
380
|
-
is_sorted = column_name == current_order_by
|
381
|
-
sort_direction = next_sort_direction(column_name, current_order_by, current_direction)
|
382
|
-
|
383
|
-
aria_sort = if is_sorted
|
384
|
-
current_direction.downcase == "asc" ? "ascending" : "descending"
|
385
|
-
else
|
386
|
-
"none"
|
387
|
-
end
|
388
|
-
|
389
|
-
# Use common_params helper to build parameters
|
390
|
-
sort_params = common_params(order_by: column_name, order_direction: sort_direction)
|
391
|
-
|
392
|
-
link_to table_path(table_name, sort_params),
|
393
|
-
class: "d-flex align-items-center text-decoration-none text-reset column-sort-link",
|
394
|
-
title: "Sort by #{column_name} (#{sort_direction.downcase})",
|
395
|
-
"aria-sort": aria_sort,
|
396
|
-
role: "button",
|
397
|
-
tabindex: "0" do
|
398
|
-
content_tag(:span, column_name, class: "column-name") +
|
399
|
-
content_tag(:span, sort_icon(column_name, current_order_by, current_direction), class: "sort-icon-container")
|
400
|
-
end
|
401
|
-
end
|
402
|
-
|
403
|
-
# Render a complete table header row with sortable columns
|
404
|
-
def render_sortable_header_row(records, order_by, order_direction, table_name, current_page, per_page, column_filters)
|
405
|
-
return content_tag(:tr) { content_tag(:th, "No columns available") } unless records&.columns
|
406
|
-
|
407
|
-
content_tag(:tr) do
|
408
|
-
# Start with action column header (sticky first column)
|
409
|
-
headers = [
|
410
|
-
content_tag(:th, class: "px-3 py-2 text-center action-column action-column-header", width: "60px", rowspan: 2) do
|
411
|
-
content_tag(:span, "Actions")
|
412
|
-
end
|
413
|
-
]
|
414
|
-
|
415
|
-
# Add all data columns
|
416
|
-
headers += records.columns.map do |column_name|
|
417
|
-
is_sorted = order_by == column_name
|
418
|
-
content_tag(:th, class: "px-3 py-2 sortable-column #{is_sorted ? 'sorted' : ''}") do
|
419
|
-
sortable_column_header(column_name, order_by, order_direction, table_name, current_page, per_page, column_filters)
|
420
|
-
end
|
421
|
-
end
|
422
|
-
|
423
|
-
headers.join.html_safe
|
424
|
-
end
|
425
|
-
end
|
426
|
-
|
427
|
-
# Render the column filters row
|
428
|
-
def render_column_filters_row(form, records, columns, column_filters)
|
429
|
-
return content_tag(:tr) { content_tag(:th, "") } unless records&.columns
|
430
|
-
|
431
|
-
content_tag(:tr, class: "column-filters") do
|
432
|
-
filters = records.columns.map do |column_name|
|
433
|
-
content_tag(:th, class: "p-0") do
|
434
|
-
render_column_filter(form, column_name, columns, column_filters)
|
435
|
-
end
|
436
|
-
end
|
437
|
-
|
438
|
-
filters.join.html_safe
|
439
|
-
end
|
440
|
-
end
|
441
|
-
|
442
|
-
# Render a cell that may include a foreign key link
|
443
|
-
def render_table_cell(cell, column_name, metadata)
|
444
|
-
cell_value = format_cell_value(cell)
|
445
|
-
foreign_key = metadata && metadata[:foreign_keys] ?
|
446
|
-
metadata[:foreign_keys].find { |fk| fk[:column] == column_name } :
|
447
|
-
nil
|
448
|
-
|
449
|
-
if foreign_key && !cell.nil?
|
450
|
-
fk_params = { column_filters: { foreign_key[:primary_key] => cell } }
|
451
|
-
fk_params = fk_params.merge(common_params.except(:column_filters))
|
452
|
-
|
453
|
-
content_tag(:td, title: "#{cell_value} (Click to view referenced record)") do
|
454
|
-
link_to(cell_value, table_path(foreign_key[:to_table], fk_params),
|
455
|
-
class: "text-decoration-none foreign-key-link") +
|
456
|
-
content_tag(:i, "", class: "bi bi-link-45deg text-muted small")
|
457
|
-
end
|
458
|
-
else
|
459
|
-
content_tag(:td, cell_value, title: cell_value)
|
460
|
-
end
|
461
|
-
end
|
462
|
-
|
463
|
-
# Render a table row with cells
|
464
|
-
def render_table_row(row, records, metadata)
|
465
|
-
content_tag(:tr) do
|
466
|
-
# Start with action column (sticky first column)
|
467
|
-
cells = [ render_action_cell(row, records.columns, metadata) ]
|
468
|
-
|
469
|
-
# Add all data cells
|
470
|
-
cells += row.each_with_index.map do |cell, cell_index|
|
471
|
-
column_name = records.columns[cell_index]
|
472
|
-
render_table_cell(cell, column_name, metadata)
|
473
|
-
end
|
474
|
-
|
475
|
-
cells.join.html_safe
|
476
|
-
end
|
477
|
-
end
|
478
|
-
|
479
|
-
# Render the entire table body with rows
|
480
|
-
def render_table_body(records, metadata)
|
481
|
-
if records.nil? || records.rows.nil? || records.empty?
|
482
|
-
content_tag(:tbody) do
|
483
|
-
content_tag(:tr) do
|
484
|
-
# Adding +1 to account for the action column
|
485
|
-
total_columns = records&.columns&.size.to_i + 1
|
486
|
-
content_tag(:td, "No records found or table is empty.", colspan: total_columns, class: "text-center")
|
487
|
-
end
|
488
|
-
end
|
489
|
-
else
|
490
|
-
content_tag(:tbody) do
|
491
|
-
records.rows.map do |row|
|
492
|
-
render_table_row(row, records, metadata)
|
493
|
-
end.join.html_safe
|
494
|
-
end
|
495
|
-
end
|
496
|
-
end
|
497
|
-
|
498
|
-
# Render action buttons for a record
|
499
|
-
def render_action_cell(row_data, columns, metadata = nil)
|
500
|
-
data_attributes = {}
|
501
|
-
|
502
|
-
# Create a hash of column_name: value pairs for data attributes
|
503
|
-
columns.each_with_index do |column_name, index|
|
504
|
-
data_attributes[column_name] = row_data[index].to_s
|
505
|
-
end
|
506
|
-
|
507
|
-
content_tag(:td, class: "text-center action-column") do
|
508
|
-
button_tag(
|
509
|
-
type: "button",
|
510
|
-
class: "btn btn-sm btn-primary view-record-btn",
|
511
|
-
title: "View Record Details",
|
512
|
-
data: {
|
513
|
-
bs_toggle: "modal",
|
514
|
-
bs_target: "#recordDetailModal",
|
515
|
-
record_data: data_attributes.to_json,
|
516
|
-
foreign_keys: metadata && metadata[:foreign_keys] ? metadata[:foreign_keys].to_json : "[]",
|
517
|
-
reverse_foreign_keys: metadata && metadata[:reverse_foreign_keys] ? metadata[:reverse_foreign_keys].to_json : "[]"
|
518
|
-
}
|
519
|
-
) do
|
520
|
-
content_tag(:i, "", class: "bi bi-eye")
|
521
|
-
end
|
522
|
-
end
|
523
|
-
end
|
3
|
+
# Include all the helper modules organized by logical concerns
|
4
|
+
include DatabaseHelper
|
5
|
+
include FilterHelper
|
6
|
+
include FormattingHelper
|
7
|
+
include PaginationHelper
|
8
|
+
include SortingHelper
|
9
|
+
include TableRenderingHelper
|
10
|
+
include NavigationHelper
|
11
|
+
include UiHelper
|
524
12
|
end
|
525
13
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module DatabaseHelper
|
3
|
+
# Check if a table has a created_at column
|
4
|
+
def has_timestamp_column?(table_name)
|
5
|
+
return false unless table_name.present?
|
6
|
+
|
7
|
+
# Get the columns for the table directly using DatabaseManager
|
8
|
+
columns = get_database_manager.table_columns(table_name)
|
9
|
+
columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Helper to access the database manager
|
13
|
+
def get_database_manager
|
14
|
+
@database_manager ||= ::Dbviewer::Database::Manager.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Extract column type from columns info
|
18
|
+
def column_type_from_info(column_name, columns)
|
19
|
+
return nil unless columns.present?
|
20
|
+
|
21
|
+
column_info = columns.find { |c| c[:name].to_s == column_name.to_s }
|
22
|
+
column_info ? column_info[:type].to_s.downcase : nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get appropriate icon for column data type
|
26
|
+
def column_type_icon(column_type)
|
27
|
+
case column_type.to_s.downcase
|
28
|
+
when /int/, /serial/, /number/, /decimal/, /float/, /double/
|
29
|
+
"bi-123"
|
30
|
+
when /char/, /text/, /string/, /uuid/
|
31
|
+
"bi-fonts"
|
32
|
+
when /date/, /time/
|
33
|
+
"bi-calendar"
|
34
|
+
when /bool/
|
35
|
+
"bi-toggle-on"
|
36
|
+
when /json/, /jsonb/
|
37
|
+
"bi-braces"
|
38
|
+
when /array/
|
39
|
+
"bi-list-ol"
|
40
|
+
else
|
41
|
+
"bi-file-earmark"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Determine if the current table should be active in the sidebar
|
46
|
+
def current_table?(table_name)
|
47
|
+
@table_name.present? && @table_name == table_name
|
48
|
+
end
|
49
|
+
|
50
|
+
# Format table name for display - truncate if too long
|
51
|
+
def format_table_name(table_name)
|
52
|
+
if table_name.length > 20
|
53
|
+
"#{table_name.first(17)}..."
|
54
|
+
else
|
55
|
+
table_name
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|