Module: Dbviewer::ApplicationHelper

Defined in:
app/helpers/dbviewer/application_helper.rb

Instance Method Summary collapse

Instance Method Details

#active_nav_class(controller_name, action_name = nil) ⇒ Object

Helper method to determine if current controller and action match



290
291
292
293
294
295
296
297
298
299
# File 'app/helpers/dbviewer/application_helper.rb', line 290

def active_nav_class(controller_name, action_name = nil)
  current_controller = params[:controller].split("/").last
  active = current_controller == controller_name

  if action_name.present?
    active = active && params[:action] == action_name
  end

  active ? "active" : ""
end

#code_block_bg_classObject

Helper method for code blocks background that adapts to dark mode



251
252
253
# File 'app/helpers/dbviewer/application_helper.rb', line 251

def code_block_bg_class
  "sql-code-block"
end

#column_type_from_info(column_name, columns) ⇒ Object

Extract column type from columns info



18
19
20
21
22
23
# File 'app/helpers/dbviewer/application_helper.rb', line 18

def column_type_from_info(column_name, columns)
  return nil unless columns.present?

  column_info = columns.find { |c| c[:name].to_s == column_name.to_s }
  column_info ? column_info[:type].to_s.downcase : nil
end

#column_type_icon(column_type) ⇒ Object

Get appropriate icon for column data type



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'app/helpers/dbviewer/application_helper.rb', line 270

def column_type_icon(column_type)
  case column_type.to_s.downcase
  when /int/, /serial/, /number/, /decimal/, /float/, /double/
    "bi-123"
  when /char/, /text/, /string/, /uuid/
    "bi-fonts"
  when /date/, /time/
    "bi-calendar"
  when /bool/
    "bi-toggle-on"
  when /json/, /jsonb/
    "bi-braces"
  when /array/
    "bi-list-ol"
  else
    "bi-file-earmark"
  end
end

#common_params(options = {}) ⇒ Object

Common parameters for pagination and filtering



149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'app/helpers/dbviewer/application_helper.rb', line 149

def common_params(options = {})
  params = {
    order_by: @order_by,
    order_direction: @order_direction,
    per_page: @per_page,
    column_filters: @column_filters
  }.merge(options)

  # Add creation filters if they exist
  params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
  params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?

  params
end

#current_table?(table_name) ⇒ Boolean

Determine if the current table should be active in the sidebar

Returns:

  • (Boolean)


256
257
258
# File 'app/helpers/dbviewer/application_helper.rb', line 256

def current_table?(table_name)
  @table_name.present? && @table_name == table_name
end

#dashboard_nav_classObject

Helper for highlighting dashboard link



302
303
304
# File 'app/helpers/dbviewer/application_helper.rb', line 302

def dashboard_nav_class
  active_nav_class("home")
end

#default_operator_for_column_type(column_type) ⇒ Object

Determine default operator based on column type



26
27
28
29
30
31
32
# File 'app/helpers/dbviewer/application_helper.rb', line 26

def default_operator_for_column_type(column_type)
  if column_type && column_type =~ /char|text|string|uuid|enum/i
    "contains"
  else
    "eq"
  end
end

#erd_nav_classObject

Helper for highlighting ERD link



312
313
314
# File 'app/helpers/dbviewer/application_helper.rb', line 312

def erd_nav_class
  active_nav_class("entity_relationship_diagrams")
end

#format_cell_value(value) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/helpers/dbviewer/application_helper.rb', line 121

def format_cell_value(value)
  return "NULL" if value.nil?
  return value.to_s.truncate(100) unless value.is_a?(String)

  case value
  when /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
    # ISO 8601 datetime
    begin
      Time.parse(value).strftime("%Y-%m-%d %H:%M:%S")
    rescue
      value.to_s.truncate(100)
    end
  when /\A\d{4}-\d{2}-\d{2}\z/
    # Date
    value
  when /\A{.+}\z/, /\A\[.+\]\z/
    # JSON
    begin
      JSON.pretty_generate(JSON.parse(value)).truncate(100)
    rescue
      value.to_s.truncate(100)
    end
  else
    value.to_s.truncate(100)
  end
end

#format_table_name(table_name) ⇒ Object

Format table name for display - truncate if too long



261
262
263
264
265
266
267
# File 'app/helpers/dbviewer/application_helper.rb', line 261

def format_table_name(table_name)
  if table_name.length > 20
    "#{table_name.first(17)}..."
  else
    table_name
  end
end

#get_database_managerObject

Helper to access the database manager



13
14
15
# File 'app/helpers/dbviewer/application_helper.rb', line 13

def get_database_manager
  @database_manager ||= ::Dbviewer::Database::Manager.new
end

#has_timestamp_column?(table_name) ⇒ Boolean

Check if a table has a created_at column

Returns:

  • (Boolean)


4
5
6
7
8
9
10
# File 'app/helpers/dbviewer/application_helper.rb', line 4

def has_timestamp_column?(table_name)
  return false unless table_name.present?

  # Get the columns for the table directly using DatabaseManager
  columns = get_database_manager.table_columns(table_name)
  columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
end

#logs_nav_classObject

Helper for highlighting SQL Logs link



317
318
319
# File 'app/helpers/dbviewer/application_helper.rb', line 317

def logs_nav_class
  active_nav_class("logs")
end

#next_sort_direction(column_name, current_order_by, current_direction) ⇒ Object

Determine the next sort direction based on the current one



332
333
334
335
336
337
338
# File 'app/helpers/dbviewer/application_helper.rb', line 332

def next_sort_direction(column_name, current_order_by, current_direction)
  if column_name == current_order_by && current_direction == "ASC"
    "DESC"
  else
    "ASC"
  end
end

#operator_options_for_column_type(column_type) ⇒ Object

Generate operator options based on column type



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'app/helpers/dbviewer/application_helper.rb', line 35

def operator_options_for_column_type(column_type)
  if column_type && (column_type =~ /datetime/ || column_type =~ /^date$/ || column_type =~ /^time$/)
    # Date/Time operators
    [
      [ "=", "eq" ],
      [ "", "neq" ],
      [ "<", "lt" ],
      [ ">", "gt" ],
      [ "", "lte" ],
      [ "", "gte" ]
    ]
  elsif column_type && column_type =~ /int|float|decimal|double|number|numeric|real|money|bigint|smallint|tinyint|mediumint|bit/i
    # Numeric operators
    [
      [ "=", "eq" ],
      [ "", "neq" ],
      [ "<", "lt" ],
      [ ">", "gt" ],
      [ "", "lte" ],
      [ "", "gte" ]
    ]
  else
    # Text operators
    [
      [ "contains", "contains" ],
      [ "not contains", "not_contains" ],
      [ "=", "eq" ],
      [ "", "neq" ],
      [ "starts with", "starts_with" ],
      [ "ends with", "ends_with" ]
    ]
  end
end

#per_page_url_params(table_name) ⇒ Object

Generate URL parameters for per-page dropdown



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'app/helpers/dbviewer/application_helper.rb', line 196

def per_page_url_params(table_name)
  # Start with the dynamic part for the select element
  url_params = "per_page=' + this.value + '&page=1"

  # Add all other common parameters except per_page and page which we already set
  params = common_params.except(:per_page, :page)

  # Convert the params hash to URL parameters
  params.each do |key, value|
    if key == :column_filters && value.is_a?(Hash) && value.reject { |_, v| v.blank? }.any?
      value.reject { |_, v| v.blank? }.each do |filter_key, filter_value|
        url_params += "&column_filters[#{filter_key}]=#{CGI.escape(filter_value.to_s)}"
      end
    elsif value.present?
      url_params += "&#{key}=#{CGI.escape(value.to_s)}"
    end
  end

  url_params
end

#render_action_cell(row_data, columns, metadata = nil) ⇒ Object

Render action buttons for a record



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'app/helpers/dbviewer/application_helper.rb', line 461

def render_action_cell(row_data, columns,  = nil)
  data_attributes = {}

  # Create a hash of column_name: value pairs for data attributes
  columns.each_with_index do |column_name, index|
    data_attributes[column_name] = row_data[index].to_s
  end

  (:td, class: "text-center action-column") do
    button_tag(
      type: "button",
      class: "btn btn-sm btn-primary view-record-btn",
      title: "View Record Details",
      data: {
        bs_toggle: "modal",
        bs_target: "#recordDetailModal",
        record_data: data_attributes.to_json,
        foreign_keys:  && [:foreign_keys] ? [:foreign_keys].to_json : "[]",
        reverse_foreign_keys:  && [:reverse_foreign_keys] ? [:reverse_foreign_keys].to_json : "[]"
      }
    ) do
      (:i, "", class: "bi bi-eye")
    end
  end
end

#render_column_filter(form, column_name, columns, column_filters) ⇒ Object

Render complete filter input group for a column



112
113
114
115
116
117
118
119
# File 'app/helpers/dbviewer/application_helper.rb', line 112

def render_column_filter(form, column_name, columns, column_filters)
  column_type = column_type_from_info(column_name, columns)

  (:div, class: "filter-input-group") do
    render_operator_select(form, column_name, column_type, column_filters) +
    render_column_filter_input(form, column_name, column_type, column_filters)
  end
end

#render_column_filter_input(form, column_name, column_type, column_filters) ⇒ Object

Render column filter input based on column type



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'app/helpers/dbviewer/application_helper.rb', line 70

def render_column_filter_input(form, column_name, column_type, column_filters)
  if column_type && column_type =~ /datetime/
    form.datetime_local_field("column_filters[#{column_name}]",
      value: column_filters[column_name],
      class: "form-control form-control-sm column-filter rounded-0",
      data: { column: column_name })
  elsif column_type && column_type =~ /^date$/
    form.date_field("column_filters[#{column_name}]",
      value: column_filters[column_name],
      class: "form-control form-control-sm column-filter rounded-0",
      data: { column: column_name })
  elsif column_type && column_type =~ /^time$/
    form.time_field("column_filters[#{column_name}]",
      value: column_filters[column_name],
      class: "form-control form-control-sm column-filter rounded-0",
      data: { column: column_name })
  else
    form.text_field("column_filters[#{column_name}]",
      value: column_filters[column_name],
      placeholder: "",
      class: "form-control form-control-sm column-filter rounded-0",
      data: { column: column_name })
  end
end

#render_column_filters_row(form, records, columns, column_filters) ⇒ Object

Render the column filters row



390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'app/helpers/dbviewer/application_helper.rb', line 390

def render_column_filters_row(form, records, columns, column_filters)
  return (:tr) { (:th, "") } unless records&.columns

  (:tr, class: "column-filters") do
    filters = records.columns.map do |column_name|
      (:th, class: "p-0") do
        render_column_filter(form, column_name, columns, column_filters)
      end
    end

    filters.join.html_safe
  end
end

#render_operator_select(form, column_name, column_type, column_filters) ⇒ Object

Render operator select for column filter



96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'app/helpers/dbviewer/application_helper.rb', line 96

def render_operator_select(form, column_name, column_type, column_filters)
  # Get previously selected operator or default
  default_operator = default_operator_for_column_type(column_type)
  selected_operator = column_filters["#{column_name}_operator"]
  selected_operator = default_operator if selected_operator.nil? || selected_operator == "default"

  # Get appropriate options
  operator_options = operator_options_for_column_type(column_type)

  form.select("column_filters[#{column_name}_operator]",
    options_for_select(operator_options, selected_operator),
    { include_blank: false },
    { class: "form-select form-select-sm operator-select" })
end

#render_pagination(table_name, current_page, total_pages, params = {}) ⇒ Object

Render pagination UI



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'app/helpers/dbviewer/application_helper.rb', line 165

def render_pagination(table_name, current_page, total_pages, params = {})
  return unless total_pages && total_pages > 1

  (:nav, 'aria-label': "Page navigation") do
    (:ul, class: "pagination justify-content-center") do
      prev_link = (:li, class: "page-item #{current_page == 1 ? 'disabled' : ''}") do
        link_to "«", table_path(table_name, params.merge(page: [ current_page - 1, 1 ].max)), class: "page-link"
      end

      # Calculate page range to display
      start_page = [ 1, current_page - 2 ].max
      end_page = [ start_page + 4, total_pages ].min
      start_page = [ 1, end_page - 4 ].max

      # Generate page links
      page_links = (start_page..end_page).map do |page_num|
        (:li, class: "page-item #{page_num == current_page ? 'active' : ''}") do
          link_to page_num, table_path(table_name, params.merge(page: page_num)), class: "page-link"
        end
      end.join.html_safe

      next_link = (:li, class: "page-item #{current_page == total_pages ? 'disabled' : ''}") do
        link_to "»", table_path(table_name, params.merge(page: [ current_page + 1, total_pages ].min)), class: "page-link"
      end

      prev_link + page_links + next_link
    end
  end
end

#render_sortable_header_row(records, order_by, order_direction, table_name, current_page, per_page, column_filters) ⇒ Object

Render a complete table header row with sortable columns



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'app/helpers/dbviewer/application_helper.rb', line 366

def render_sortable_header_row(records, order_by, order_direction, table_name, current_page, per_page, column_filters)
  return (:tr) { (:th, "No columns available") } unless records&.columns

  (:tr) do
    # Start with action column header (sticky first column)
    headers = [
      (:th, class: "px-3 py-2 text-center action-column action-column-header", width: "60px", rowspan: 2) do
        (:span, "Actions")
      end
    ]

    # Add all data columns
    headers += records.columns.map do |column_name|
      is_sorted = order_by == column_name
      (:th, class: "px-3 py-2 sortable-column #{is_sorted ? 'sorted' : ''}") do
        sortable_column_header(column_name, order_by, order_direction, table_name, current_page, per_page, column_filters)
      end
    end

    headers.join.html_safe
  end
end

#render_table_body(records, metadata) ⇒ Object

Render the entire table body with rows



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'app/helpers/dbviewer/application_helper.rb', line 442

def render_table_body(records, )
  if records.nil? || records.rows.nil? || records.empty?
    (:tbody) do
      (:tr) do
        # Adding +1 to account for the action column
        total_columns = records&.columns&.size.to_i + 1
        (:td, "No records found or table is empty.", colspan: total_columns, class: "text-center")
      end
    end
  else
    (:tbody) do
      records.rows.map do |row|
        render_table_row(row, records, )
      end.join.html_safe
    end
  end
end

#render_table_cell(cell, column_name, metadata) ⇒ Object

Render a cell that may include a foreign key link



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'app/helpers/dbviewer/application_helper.rb', line 405

def render_table_cell(cell, column_name, )
  cell_value = format_cell_value(cell)
  foreign_key =  && [:foreign_keys] ?
                [:foreign_keys].find { |fk| fk[:column] == column_name } :
                nil

  if foreign_key && !cell.nil?
    fk_params = { column_filters: { foreign_key[:primary_key] => cell } }
    fk_params = fk_params.merge(common_params.except(:column_filters))

    (:td, title: "#{cell_value} (Click to view referenced record)") do
      link_to(cell_value, table_path(foreign_key[:to_table], fk_params),
              class: "text-decoration-none foreign-key-link") +
      (:i, "", class: "bi bi-link-45deg text-muted small")
    end
  else
    (:td, cell_value, title: cell_value)
  end
end

#render_table_row(row, records, metadata) ⇒ Object

Render a table row with cells



426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'app/helpers/dbviewer/application_helper.rb', line 426

def render_table_row(row, records, )
  (:tr) do
    # Start with action column (sticky first column)
    cells = [ render_action_cell(row, records.columns, ) ]

    # Add all data cells
    cells += row.each_with_index.map do |cell, cell_index|
      column_name = records.columns[cell_index]
      render_table_cell(cell, column_name, )
    end

    cells.join.html_safe
  end
end

#sort_icon(column_name, current_order_by, current_direction) ⇒ Object

Returns a sort icon based on the current sort direction



322
323
324
325
326
327
328
329
# File 'app/helpers/dbviewer/application_helper.rb', line 322

def sort_icon(column_name, current_order_by, current_direction)
  if column_name == current_order_by
    direction = current_direction == "ASC" ? "up" : "down"
    "<i class='bi bi-sort-#{direction}'></i>".html_safe
  else
    "<i class='bi bi-filter invisible sort-icon'></i>".html_safe
  end
end

#sortable_column_header(column_name, current_order_by, current_direction, table_name, current_page, per_page, column_filters) ⇒ Object

Generate a sortable column header link



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'app/helpers/dbviewer/application_helper.rb', line 341

def sortable_column_header(column_name, current_order_by, current_direction, table_name, current_page, per_page, column_filters)
  is_sorted = column_name == current_order_by
  sort_direction = next_sort_direction(column_name, current_order_by, current_direction)

  aria_sort = if is_sorted
    current_direction.downcase == "asc" ? "ascending" : "descending"
  else
    "none"
  end

  # Use common_params helper to build parameters
  sort_params = common_params(order_by: column_name, order_direction: sort_direction)

  link_to table_path(table_name, sort_params),
    class: "d-flex align-items-center text-decoration-none text-reset column-sort-link",
    title: "Sort by #{column_name} (#{sort_direction.downcase})",
    "aria-sort": aria_sort,
    role: "button",
    tabindex: "0" do
      (:span, column_name, class: "column-name") +
      (:span, sort_icon(column_name, current_order_by, current_direction), class: "sort-icon-container")
  end
end

#stat_card_bg_classObject

Returns the appropriate background class for stat cards that adapts to dark mode



246
247
248
# File 'app/helpers/dbviewer/application_helper.rb', line 246

def stat_card_bg_class
  "stat-card-bg"
end

#tables_nav_classObject

Helper for highlighting tables link



307
308
309
# File 'app/helpers/dbviewer/application_helper.rb', line 307

def tables_nav_class
  active_nav_class("tables")
end

#theme_toggle_iconObject

Returns the theme toggle icon based on the current theme



236
237
238
# File 'app/helpers/dbviewer/application_helper.rb', line 236

def theme_toggle_icon
  '<i class="bi bi-moon"></i><i class="bi bi-sun"></i>'.html_safe
end

#theme_toggle_labelObject

Returns the aria label for the theme toggle button



241
242
243
# File 'app/helpers/dbviewer/application_helper.rb', line 241

def theme_toggle_label
  "Toggle dark mode"
end

Render time grouping links



218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'app/helpers/dbviewer/application_helper.rb', line 218

def time_grouping_links(table_name, current_grouping)
  params = common_params

  (:div, class: "btn-group btn-group-sm", role: "group", 'aria-label': "Time grouping") do
    [
      link_to("Hourly", table_path(table_name, params.merge(time_group: "hourly")),
             class: "btn btn-outline-primary #{current_grouping == 'hourly' ? 'active' : ''}"),
      link_to("Daily", table_path(table_name, params.merge(time_group: "daily")),
             class: "btn btn-outline-primary #{current_grouping == 'daily' ? 'active' : ''}"),
      link_to("Weekly", table_path(table_name, params.merge(time_group: "weekly")),
             class: "btn btn-outline-primary #{current_grouping == 'weekly' ? 'active' : ''}")
    ].join.html_safe
  end
end