dbviewer 0.6.3 → 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 -503
- 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 +1 -1
- data/app/controllers/dbviewer/tables_controller.rb +7 -2
- data/app/helpers/dbviewer/application_helper.rb +9 -541
- 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/lib/dbviewer/database/manager.rb +2 -3
- data/lib/dbviewer/datatable/query_operations.rb +35 -20
- data/lib/dbviewer/query/executor.rb +0 -35
- data/lib/dbviewer/version.rb +1 -1
- metadata +16 -1
@@ -0,0 +1,137 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module FilterHelper
|
3
|
+
# Determine default operator based on column type
|
4
|
+
def default_operator_for_column_type(column_type)
|
5
|
+
if column_type && column_type =~ /char|text|string|uuid|enum/i
|
6
|
+
"contains"
|
7
|
+
else
|
8
|
+
"eq"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Generate operator options based on column type
|
13
|
+
def operator_options_for_column_type(column_type)
|
14
|
+
# Common operators for all types
|
15
|
+
common_operators = [
|
16
|
+
[ "is null", "is_null" ],
|
17
|
+
[ "is not null", "is_not_null" ]
|
18
|
+
]
|
19
|
+
|
20
|
+
type_specific_operators = if column_type && (column_type =~ /datetime/ || column_type =~ /^date$/ || column_type =~ /^time$/)
|
21
|
+
# Date/Time operators
|
22
|
+
[
|
23
|
+
[ "=", "eq" ],
|
24
|
+
[ "≠", "neq" ],
|
25
|
+
[ "<", "lt" ],
|
26
|
+
[ ">", "gt" ],
|
27
|
+
[ "≤", "lte" ],
|
28
|
+
[ "≥", "gte" ]
|
29
|
+
]
|
30
|
+
elsif column_type && column_type =~ /int|float|decimal|double|number|numeric|real|money|bigint|smallint|tinyint|mediumint|bit/i
|
31
|
+
# Numeric operators
|
32
|
+
[
|
33
|
+
[ "=", "eq" ],
|
34
|
+
[ "≠", "neq" ],
|
35
|
+
[ "<", "lt" ],
|
36
|
+
[ ">", "gt" ],
|
37
|
+
[ "≤", "lte" ],
|
38
|
+
[ "≥", "gte" ]
|
39
|
+
]
|
40
|
+
else
|
41
|
+
# Text operators
|
42
|
+
[
|
43
|
+
[ "contains", "contains" ],
|
44
|
+
[ "not contains", "not_contains" ],
|
45
|
+
[ "=", "eq" ],
|
46
|
+
[ "≠", "neq" ],
|
47
|
+
[ "starts with", "starts_with" ],
|
48
|
+
[ "ends with", "ends_with" ]
|
49
|
+
]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Return type-specific operators first, then common operators
|
53
|
+
type_specific_operators + common_operators
|
54
|
+
end
|
55
|
+
|
56
|
+
# Render column filter input based on column type
|
57
|
+
def render_column_filter_input(form, column_name, column_type, column_filters)
|
58
|
+
# Get selected operator to check if it's a null operator
|
59
|
+
operator = column_filters["#{column_name}_operator"]
|
60
|
+
is_null_operator = operator == "is_null" || operator == "is_not_null"
|
61
|
+
|
62
|
+
# Clean up the value for non-null operators if the value contains a null operator
|
63
|
+
# This ensures we don't carry over 'is_null' or 'is_not_null' values when switching operators
|
64
|
+
value = column_filters[column_name]
|
65
|
+
if !is_null_operator && value.present? && (value == "is_null" || value == "is_not_null")
|
66
|
+
value = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# For null operators, display a non-editable field without placeholder
|
70
|
+
if is_null_operator
|
71
|
+
# Keep a hidden field for the actual value
|
72
|
+
hidden_field = form.hidden_field("column_filters[#{column_name}]",
|
73
|
+
value: operator,
|
74
|
+
class: "null-filter-value",
|
75
|
+
data: { column: column_name })
|
76
|
+
|
77
|
+
# Add a visible but disabled text field with no placeholder or value
|
78
|
+
visible_field = form.text_field("column_filters[#{column_name}_display]",
|
79
|
+
disabled: true,
|
80
|
+
value: "",
|
81
|
+
class: "form-control form-control-sm column-filter rounded-0 disabled-filter",
|
82
|
+
data: { column: "#{column_name}_display" })
|
83
|
+
|
84
|
+
hidden_field + visible_field
|
85
|
+
elsif column_type && column_type =~ /datetime/
|
86
|
+
form.datetime_local_field("column_filters[#{column_name}]",
|
87
|
+
value: value,
|
88
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
89
|
+
data: { column: column_name })
|
90
|
+
elsif column_type && column_type =~ /^date$/
|
91
|
+
form.date_field("column_filters[#{column_name}]",
|
92
|
+
value: value,
|
93
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
94
|
+
data: { column: column_name })
|
95
|
+
elsif column_type && column_type =~ /^time$/
|
96
|
+
form.time_field("column_filters[#{column_name}]",
|
97
|
+
value: value,
|
98
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
99
|
+
data: { column: column_name })
|
100
|
+
else
|
101
|
+
form.text_field("column_filters[#{column_name}]",
|
102
|
+
value: value,
|
103
|
+
placeholder: "",
|
104
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
105
|
+
data: { column: column_name })
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Render operator select for column filter
|
110
|
+
def render_operator_select(form, column_name, column_type, column_filters)
|
111
|
+
# Get previously selected operator or default
|
112
|
+
default_operator = default_operator_for_column_type(column_type)
|
113
|
+
selected_operator = column_filters["#{column_name}_operator"]
|
114
|
+
selected_operator = default_operator if selected_operator.nil? || selected_operator == "default"
|
115
|
+
|
116
|
+
# Get appropriate options
|
117
|
+
operator_options = operator_options_for_column_type(column_type)
|
118
|
+
|
119
|
+
form.select("column_filters[#{column_name}_operator]",
|
120
|
+
options_for_select(operator_options, selected_operator),
|
121
|
+
{ include_blank: false },
|
122
|
+
{ class: "form-select form-select-sm operator-select" })
|
123
|
+
end
|
124
|
+
|
125
|
+
# Render complete filter input group for a column
|
126
|
+
def render_column_filter(form, column_name, columns, column_filters)
|
127
|
+
column_type = column_type_from_info(column_name, columns)
|
128
|
+
|
129
|
+
content_tag(:div, class: "filter-input-group") do
|
130
|
+
operator_select = render_operator_select(form, column_name, column_type, column_filters)
|
131
|
+
input_field = render_column_filter_input(form, column_name, column_type, column_filters)
|
132
|
+
|
133
|
+
operator_select + input_field
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module FormattingHelper
|
3
|
+
def format_cell_value(value)
|
4
|
+
return "NULL" if value.nil?
|
5
|
+
return value.to_s.truncate(100) unless value.is_a?(String)
|
6
|
+
|
7
|
+
case value
|
8
|
+
when /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
|
9
|
+
# ISO 8601 datetime
|
10
|
+
begin
|
11
|
+
Time.parse(value).strftime("%Y-%m-%d %H:%M:%S")
|
12
|
+
rescue
|
13
|
+
value.to_s.truncate(100)
|
14
|
+
end
|
15
|
+
when /\A\d{4}-\d{2}-\d{2}\z/
|
16
|
+
# Date
|
17
|
+
value
|
18
|
+
when /\A{.+}\z/, /\A\[.+\]\z/
|
19
|
+
# JSON
|
20
|
+
begin
|
21
|
+
JSON.pretty_generate(JSON.parse(value)).truncate(100)
|
22
|
+
rescue
|
23
|
+
value.to_s.truncate(100)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
value.to_s.truncate(100)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module NavigationHelper
|
3
|
+
# Helper method to determine if current controller and action match
|
4
|
+
def active_nav_class(controller_name, action_name = nil)
|
5
|
+
current_controller = params[:controller].split("/").last
|
6
|
+
active = current_controller == controller_name
|
7
|
+
|
8
|
+
if action_name.present?
|
9
|
+
active = active && params[:action] == action_name
|
10
|
+
end
|
11
|
+
|
12
|
+
active ? "active" : ""
|
13
|
+
end
|
14
|
+
|
15
|
+
# Helper for highlighting dashboard link
|
16
|
+
def dashboard_nav_class
|
17
|
+
active_nav_class("home")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Helper for highlighting tables link
|
21
|
+
def tables_nav_class
|
22
|
+
active_nav_class("tables")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Helper for highlighting ERD link
|
26
|
+
def erd_nav_class
|
27
|
+
active_nav_class("entity_relationship_diagrams")
|
28
|
+
end
|
29
|
+
|
30
|
+
# Helper for highlighting SQL Logs link
|
31
|
+
def logs_nav_class
|
32
|
+
active_nav_class("logs")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module PaginationHelper
|
3
|
+
# Common parameters for pagination and filtering
|
4
|
+
def common_params(options = {})
|
5
|
+
params = {
|
6
|
+
order_by: @order_by,
|
7
|
+
order_direction: @order_direction,
|
8
|
+
per_page: @per_page,
|
9
|
+
column_filters: @column_filters
|
10
|
+
}.merge(options)
|
11
|
+
|
12
|
+
# Add creation filters if they exist
|
13
|
+
params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
14
|
+
params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
15
|
+
|
16
|
+
params
|
17
|
+
end
|
18
|
+
|
19
|
+
# Render pagination UI
|
20
|
+
def render_pagination(table_name, current_page, total_pages, params = {})
|
21
|
+
return unless total_pages && total_pages > 1
|
22
|
+
|
23
|
+
content_tag(:nav, 'aria-label': "Page navigation") do
|
24
|
+
content_tag(:ul, class: "pagination justify-content-center") do
|
25
|
+
prev_link = content_tag(:li, class: "page-item #{current_page == 1 ? 'disabled' : ''}") do
|
26
|
+
link_to "«", table_path(table_name, params.merge(page: [ current_page - 1, 1 ].max)), class: "page-link"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Calculate page range to display
|
30
|
+
start_page = [ 1, current_page - 2 ].max
|
31
|
+
end_page = [ start_page + 4, total_pages ].min
|
32
|
+
start_page = [ 1, end_page - 4 ].max
|
33
|
+
|
34
|
+
# Generate page links
|
35
|
+
page_links = (start_page..end_page).map do |page_num|
|
36
|
+
content_tag(:li, class: "page-item #{page_num == current_page ? 'active' : ''}") do
|
37
|
+
link_to page_num, table_path(table_name, params.merge(page: page_num)), class: "page-link"
|
38
|
+
end
|
39
|
+
end.join.html_safe
|
40
|
+
|
41
|
+
next_link = content_tag(:li, class: "page-item #{current_page == total_pages ? 'disabled' : ''}") do
|
42
|
+
link_to "»", table_path(table_name, params.merge(page: [ current_page + 1, total_pages ].min)), class: "page-link"
|
43
|
+
end
|
44
|
+
|
45
|
+
prev_link + page_links + next_link
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Generate URL parameters for per-page dropdown
|
51
|
+
def per_page_url_params(table_name)
|
52
|
+
# Start with the dynamic part for the select element
|
53
|
+
url_params = "per_page=' + this.value + '&page=1"
|
54
|
+
|
55
|
+
# Add all other common parameters except per_page and page which we already set
|
56
|
+
params = common_params.except(:per_page, :page)
|
57
|
+
|
58
|
+
# Convert the params hash to URL parameters
|
59
|
+
params.each do |key, value|
|
60
|
+
if key == :column_filters && value.is_a?(Hash) && value.reject { |_, v| v.blank? }.any?
|
61
|
+
value.reject { |_, v| v.blank? }.each do |filter_key, filter_value|
|
62
|
+
url_params += "&column_filters[#{filter_key}]=#{CGI.escape(filter_value.to_s)}"
|
63
|
+
end
|
64
|
+
elsif value.present?
|
65
|
+
url_params += "&#{key}=#{CGI.escape(value.to_s)}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
url_params
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module SortingHelper
|
3
|
+
# Returns a sort icon based on the current sort direction
|
4
|
+
def sort_icon(column_name, current_order_by, current_direction)
|
5
|
+
if column_name == current_order_by
|
6
|
+
direction = current_direction == "ASC" ? "up" : "down"
|
7
|
+
"<i class='bi bi-sort-#{direction}'></i>".html_safe
|
8
|
+
else
|
9
|
+
"<i class='bi bi-filter invisible sort-icon'></i>".html_safe
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Determine the next sort direction based on the current one
|
14
|
+
def next_sort_direction(column_name, current_order_by, current_direction)
|
15
|
+
if column_name == current_order_by && current_direction == "ASC"
|
16
|
+
"DESC"
|
17
|
+
else
|
18
|
+
"ASC"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Generate a sortable column header link
|
23
|
+
def sortable_column_header(column_name, current_order_by, current_direction, table_name, current_page, per_page, column_filters)
|
24
|
+
is_sorted = column_name == current_order_by
|
25
|
+
sort_direction = next_sort_direction(column_name, current_order_by, current_direction)
|
26
|
+
|
27
|
+
aria_sort = if is_sorted
|
28
|
+
current_direction.downcase == "asc" ? "ascending" : "descending"
|
29
|
+
else
|
30
|
+
"none"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Use common_params helper to build parameters
|
34
|
+
sort_params = common_params(order_by: column_name, order_direction: sort_direction)
|
35
|
+
|
36
|
+
link_to table_path(table_name, sort_params),
|
37
|
+
class: "d-flex align-items-center text-decoration-none text-reset column-sort-link",
|
38
|
+
title: "Sort by #{column_name} (#{sort_direction.downcase})",
|
39
|
+
"aria-sort": aria_sort,
|
40
|
+
role: "button",
|
41
|
+
tabindex: "0" do
|
42
|
+
content_tag(:span, column_name, class: "column-name") +
|
43
|
+
content_tag(:span, sort_icon(column_name, current_order_by, current_direction), class: "sort-icon-container")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module TableRenderingHelper
|
3
|
+
# Render a complete table header row with sortable columns
|
4
|
+
def render_sortable_header_row(records, order_by, order_direction, table_name, current_page, per_page, column_filters)
|
5
|
+
return content_tag(:tr) { content_tag(:th, "No columns available") } unless records&.columns
|
6
|
+
|
7
|
+
content_tag(:tr) do
|
8
|
+
# Start with action column header (sticky first column)
|
9
|
+
headers = [
|
10
|
+
content_tag(:th, class: "px-3 py-2 text-center action-column action-column-header", width: "60px", rowspan: 2) do
|
11
|
+
content_tag(:span, "Actions")
|
12
|
+
end
|
13
|
+
]
|
14
|
+
|
15
|
+
# Add all data columns
|
16
|
+
headers += records.columns.map do |column_name|
|
17
|
+
is_sorted = order_by == column_name
|
18
|
+
content_tag(:th, class: "px-3 py-2 sortable-column #{is_sorted ? 'sorted' : ''}") do
|
19
|
+
sortable_column_header(column_name, order_by, order_direction, table_name, current_page, per_page, column_filters)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
headers.join.html_safe
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Render the column filters row
|
28
|
+
def render_column_filters_row(form, records, columns, column_filters)
|
29
|
+
return content_tag(:tr) { content_tag(:th, "") } unless records&.columns
|
30
|
+
|
31
|
+
content_tag(:tr, class: "column-filters") do
|
32
|
+
filters = records.columns.map do |column_name|
|
33
|
+
content_tag(:th, class: "p-0") do
|
34
|
+
render_column_filter(form, column_name, columns, column_filters)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
filters.join.html_safe
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Render a cell that may include a foreign key link
|
43
|
+
def render_table_cell(cell, column_name, metadata)
|
44
|
+
cell_value = format_cell_value(cell)
|
45
|
+
foreign_key = metadata && metadata[:foreign_keys] ?
|
46
|
+
metadata[:foreign_keys].find { |fk| fk[:column] == column_name } :
|
47
|
+
nil
|
48
|
+
|
49
|
+
if foreign_key && !cell.nil?
|
50
|
+
fk_params = { column_filters: { foreign_key[:primary_key] => cell } }
|
51
|
+
fk_params = fk_params.merge(common_params.except(:column_filters))
|
52
|
+
|
53
|
+
content_tag(:td, title: "#{cell_value} (Click to view referenced record)") do
|
54
|
+
link_to(cell_value, table_path(foreign_key[:to_table], fk_params),
|
55
|
+
class: "text-decoration-none foreign-key-link") +
|
56
|
+
content_tag(:i, "", class: "bi bi-link-45deg text-muted small")
|
57
|
+
end
|
58
|
+
else
|
59
|
+
content_tag(:td, cell_value, title: cell_value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Render a table row with cells
|
64
|
+
def render_table_row(row, records, metadata)
|
65
|
+
content_tag(:tr) do
|
66
|
+
# Start with action column (sticky first column)
|
67
|
+
cells = [ render_action_cell(row, records.columns, metadata) ]
|
68
|
+
|
69
|
+
# Add all data cells
|
70
|
+
cells += row.each_with_index.map do |cell, cell_index|
|
71
|
+
column_name = records.columns[cell_index]
|
72
|
+
render_table_cell(cell, column_name, metadata)
|
73
|
+
end
|
74
|
+
|
75
|
+
cells.join.html_safe
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Render the entire table body with rows
|
80
|
+
def render_table_body(records, metadata)
|
81
|
+
if records.nil? || records.rows.nil? || records.empty?
|
82
|
+
content_tag(:tbody) do
|
83
|
+
content_tag(:tr) do
|
84
|
+
# Adding +1 to account for the action column
|
85
|
+
total_columns = records&.columns&.size.to_i + 1
|
86
|
+
content_tag(:td, "No records found or table is empty.", colspan: total_columns, class: "text-center")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
else
|
90
|
+
content_tag(:tbody) do
|
91
|
+
records.rows.map do |row|
|
92
|
+
render_table_row(row, records, metadata)
|
93
|
+
end.join.html_safe
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Render action buttons for a record
|
99
|
+
def render_action_cell(row_data, columns, metadata = nil)
|
100
|
+
data_attributes = {}
|
101
|
+
|
102
|
+
# Create a hash of column_name: value pairs for data attributes
|
103
|
+
columns.each_with_index do |column_name, index|
|
104
|
+
data_attributes[column_name] = row_data[index].to_s
|
105
|
+
end
|
106
|
+
|
107
|
+
content_tag(:td, class: "text-center action-column") do
|
108
|
+
content_tag(:div, class: "d-flex gap-1 justify-content-center") do
|
109
|
+
# View Record button (existing)
|
110
|
+
view_button = button_tag(
|
111
|
+
type: "button",
|
112
|
+
class: "btn btn-sm btn-primary view-record-btn",
|
113
|
+
title: "View Record Details",
|
114
|
+
data: {
|
115
|
+
bs_toggle: "modal",
|
116
|
+
bs_target: "#recordDetailModal",
|
117
|
+
record_data: data_attributes.to_json,
|
118
|
+
foreign_keys: metadata && metadata[:foreign_keys] ? metadata[:foreign_keys].to_json : "[]",
|
119
|
+
reverse_foreign_keys: metadata && metadata[:reverse_foreign_keys] ? metadata[:reverse_foreign_keys].to_json : "[]"
|
120
|
+
}
|
121
|
+
) do
|
122
|
+
content_tag(:i, "", class: "bi bi-eye")
|
123
|
+
end
|
124
|
+
|
125
|
+
# Copy FactoryBot button (new)
|
126
|
+
copy_factory_button = button_tag(
|
127
|
+
type: "button",
|
128
|
+
class: "btn btn-sm btn-outline-secondary copy-factory-btn",
|
129
|
+
title: "Copy to JSON",
|
130
|
+
data: {
|
131
|
+
record_data: data_attributes.to_json,
|
132
|
+
table_name: @table_name
|
133
|
+
},
|
134
|
+
onclick: "copyToJson(this)"
|
135
|
+
) do
|
136
|
+
content_tag(:i, "", class: "bi bi-clipboard")
|
137
|
+
end
|
138
|
+
|
139
|
+
# Concatenate both buttons
|
140
|
+
view_button + copy_factory_button
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module UiHelper
|
3
|
+
# Dark mode helper methods
|
4
|
+
|
5
|
+
# Returns the theme toggle icon based on the current theme
|
6
|
+
def theme_toggle_icon
|
7
|
+
'<i class="bi bi-moon"></i><i class="bi bi-sun"></i>'.html_safe
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns the aria label for the theme toggle button
|
11
|
+
def theme_toggle_label
|
12
|
+
"Toggle dark mode"
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the appropriate background class for stat cards that adapts to dark mode
|
16
|
+
def stat_card_bg_class
|
17
|
+
"stat-card-bg"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Helper method for code blocks background that adapts to dark mode
|
21
|
+
def code_block_bg_class
|
22
|
+
"sql-code-block"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Render time grouping links
|
26
|
+
def time_grouping_links(table_name, current_grouping)
|
27
|
+
params = common_params
|
28
|
+
|
29
|
+
content_tag(:div, class: "btn-group btn-group-sm", role: "group", 'aria-label': "Time grouping") do
|
30
|
+
[
|
31
|
+
link_to("Hourly", table_path(table_name, params.merge(time_group: "hourly")),
|
32
|
+
class: "btn btn-outline-primary #{current_grouping == 'hourly' ? 'active' : ''}"),
|
33
|
+
link_to("Daily", table_path(table_name, params.merge(time_group: "daily")),
|
34
|
+
class: "btn btn-outline-primary #{current_grouping == 'daily' ? 'active' : ''}"),
|
35
|
+
link_to("Weekly", table_path(table_name, params.merge(time_group: "weekly")),
|
36
|
+
class: "btn btn-outline-primary #{current_grouping == 'weekly' ? 'active' : ''}")
|
37
|
+
].join.html_safe
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -17,7 +17,6 @@ module Dbviewer
|
|
17
17
|
@table_query_operations = ::Dbviewer::Datatable::QueryOperations.new(
|
18
18
|
@connection,
|
19
19
|
@dynamic_model_factory,
|
20
|
-
@query_executor,
|
21
20
|
@table_metadata_manager
|
22
21
|
)
|
23
22
|
reset_cache_if_needed
|
@@ -108,7 +107,7 @@ module Dbviewer
|
|
108
107
|
# @return [ActiveRecord::Result] Result set with columns and rows
|
109
108
|
# @raise [StandardError] If the query is invalid or unsafe
|
110
109
|
def execute_query(sql)
|
111
|
-
@
|
110
|
+
@query_executor.execute_query(sql)
|
112
111
|
end
|
113
112
|
|
114
113
|
# Execute a SQLite PRAGMA command without adding a LIMIT clause
|
@@ -116,7 +115,7 @@ module Dbviewer
|
|
116
115
|
# @return [ActiveRecord::Result] Result set with the PRAGMA value
|
117
116
|
# @raise [StandardError] If the query is invalid or cannot be executed
|
118
117
|
def execute_sqlite_pragma(pragma)
|
119
|
-
@
|
118
|
+
@query_executor.execute_sqlite_pragma(pragma)
|
120
119
|
end
|
121
120
|
|
122
121
|
# Get table indexes
|
@@ -8,13 +8,11 @@ module Dbviewer
|
|
8
8
|
# Initialize with dependencies
|
9
9
|
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] The database connection
|
10
10
|
# @param dynamic_model_factory [Dbviewer::Database::DynamicModelFactory] Factory for creating dynamic AR models
|
11
|
-
# @param query_executor [Dbviewer::Query::Executor] Executor for raw SQL queries
|
12
11
|
# @param table_metadata_manager [Dbviewer::Database::MetadataManager] Manager for table metadata
|
13
|
-
def initialize(connection, dynamic_model_factory,
|
12
|
+
def initialize(connection, dynamic_model_factory, table_metadata_manager)
|
14
13
|
@connection = connection
|
15
14
|
@adapter_name = connection.adapter_name.downcase
|
16
15
|
@dynamic_model_factory = dynamic_model_factory
|
17
|
-
@query_executor = query_executor
|
18
16
|
@table_metadata_manager = table_metadata_manager
|
19
17
|
@query_analyzer = ::Dbviewer::Query::Analyzer.new(connection)
|
20
18
|
end
|
@@ -49,7 +47,7 @@ module Dbviewer
|
|
49
47
|
column_names = table_columns(table_name).map { |c| c[:name] }
|
50
48
|
|
51
49
|
# Format results
|
52
|
-
|
50
|
+
to_result_set(records, column_names)
|
53
51
|
rescue => e
|
54
52
|
Rails.logger.error("[DBViewer] Error executing table query: #{e.message}")
|
55
53
|
raise e
|
@@ -92,22 +90,6 @@ module Dbviewer
|
|
92
90
|
|
93
91
|
## -- Delegator
|
94
92
|
|
95
|
-
# Execute a raw SQL query after validating for safety
|
96
|
-
# @param sql [String] SQL query to execute
|
97
|
-
# @return [ActiveRecord::Result] Result set with columns and rows
|
98
|
-
# @raise [StandardError] If the query is invalid or unsafe
|
99
|
-
def execute_query(sql)
|
100
|
-
@query_executor.execute_query(sql)
|
101
|
-
end
|
102
|
-
|
103
|
-
# Execute a SQLite PRAGMA command without adding a LIMIT clause
|
104
|
-
# @param pragma [String] PRAGMA command to execute (without the "PRAGMA" keyword)
|
105
|
-
# @return [ActiveRecord::Result] Result set with the PRAGMA value
|
106
|
-
# @raise [StandardError] If the query is invalid or cannot be executed
|
107
|
-
def execute_sqlite_pragma(pragma)
|
108
|
-
@query_executor.execute_sqlite_pragma(pragma)
|
109
|
-
end
|
110
|
-
|
111
93
|
# Analyze query patterns and return performance recommendations
|
112
94
|
# @param table_name [String] Name of the table
|
113
95
|
# @param query_params [Dbviewer::Datatable::QueryParams] Query parameters
|
@@ -200,6 +182,39 @@ module Dbviewer
|
|
200
182
|
def primary_key(table_name)
|
201
183
|
@table_metadata_manager.primary_key(table_name)
|
202
184
|
end
|
185
|
+
|
186
|
+
# Convert ActiveRecord::Relation to a standard result format
|
187
|
+
# @param records [ActiveRecord::Relation] Records to convert
|
188
|
+
# @param column_names [Array<String>] Column names
|
189
|
+
# @return [ActiveRecord::Result] Result set with columns and rows
|
190
|
+
def to_result_set(records, column_names)
|
191
|
+
rows = records.map do |record|
|
192
|
+
column_names.map do |col|
|
193
|
+
# Handle serialized attributes
|
194
|
+
value = record[col]
|
195
|
+
serialize_if_needed(value)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
ActiveRecord::Result.new(column_names, rows)
|
200
|
+
rescue => e
|
201
|
+
Rails.logger.error("[DBViewer] Error converting to result set: #{e.message}")
|
202
|
+
ActiveRecord::Result.new([], [])
|
203
|
+
end
|
204
|
+
|
205
|
+
# Serialize complex objects for display
|
206
|
+
# @param value [Object] Value to serialize
|
207
|
+
# @return [String, Object] Serialized value or original value
|
208
|
+
def serialize_if_needed(value)
|
209
|
+
case value
|
210
|
+
when Hash, Array
|
211
|
+
value.to_json rescue value.to_s
|
212
|
+
when Time, Date, DateTime
|
213
|
+
value.to_s
|
214
|
+
else
|
215
|
+
value
|
216
|
+
end
|
217
|
+
end
|
203
218
|
end
|
204
219
|
end
|
205
220
|
end
|
@@ -53,41 +53,6 @@ module Dbviewer
|
|
53
53
|
Rails.logger.error("[DBViewer] SQLite pragma error: #{e.message} for pragma: #{pragma}")
|
54
54
|
raise e
|
55
55
|
end
|
56
|
-
|
57
|
-
# Convert ActiveRecord::Relation to a standard result format
|
58
|
-
# @param records [ActiveRecord::Relation] Records to convert
|
59
|
-
# @param column_names [Array<String>] Column names
|
60
|
-
# @return [ActiveRecord::Result] Result set with columns and rows
|
61
|
-
def to_result_set(records, column_names)
|
62
|
-
rows = records.map do |record|
|
63
|
-
column_names.map do |col|
|
64
|
-
# Handle serialized attributes
|
65
|
-
value = record[col]
|
66
|
-
serialize_if_needed(value)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
ActiveRecord::Result.new(column_names, rows)
|
71
|
-
rescue => e
|
72
|
-
Rails.logger.error("[DBViewer] Error converting to result set: #{e.message}")
|
73
|
-
ActiveRecord::Result.new([], [])
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
|
78
|
-
# Serialize complex objects for display
|
79
|
-
# @param value [Object] Value to serialize
|
80
|
-
# @return [String, Object] Serialized value or original value
|
81
|
-
def serialize_if_needed(value)
|
82
|
-
case value
|
83
|
-
when Hash, Array
|
84
|
-
value.to_json rescue value.to_s
|
85
|
-
when Time, Date, DateTime
|
86
|
-
value.to_s
|
87
|
-
else
|
88
|
-
value
|
89
|
-
end
|
90
|
-
end
|
91
56
|
end
|
92
57
|
end
|
93
58
|
end
|
data/lib/dbviewer/version.rb
CHANGED