dbviewer 0.3.3 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e6fcb0f5760c3daa24b0bbcd6f7f39d308890386eb7bae0e99b59c5027c0853
4
- data.tar.gz: 486266a6f7d882f28b2411860466cb271b7e5d32ca65a25f04bf27601a6fb4c7
3
+ metadata.gz: c368abfe3b3131b1df3b3234e1e560c2cb5f715c035680d3848ff5ff07d99cc3
4
+ data.tar.gz: 7f9b27e43d86b0181f14d9deceebcb1d6624b22cb51885ab5c3b929814cb97f9
5
5
  SHA512:
6
- metadata.gz: 362be7d7fed295db73faad72e0b339d7b1f824d945e7c141c2cb43305d9d31c109526c05fe612135c6cfafc74e618621125a03bab3127ac52ec57a58bfcabbf0
7
- data.tar.gz: 0a7289d157e0911d0e5e932c7ebbd964536ade2b125d4de7308fb518516793a6162f690a6d7c0dbc3cb88087754a3b2593265c5c6fa48c9980bb6ca0375cf91c
6
+ metadata.gz: 05f35000d4c57ec2e17e0c0fe56d49520add9f0c061debc25f5f6407ec1e52ece7a1d4361c0b92d283135492bc78d86890083e9781a30705ebcddc0fce2e58df
7
+ data.tar.gz: a76035a207c87105f9331dc4246a133da9941a5a9e435133b80254fc0037578bc69b5dbb17cb40113049ba50625d3a81816df118015446af8e9c368127e481c9
@@ -139,15 +139,25 @@ module Dbviewer
139
139
 
140
140
  # Fetch records for a table with pagination and sorting
141
141
  def fetch_table_records(table_name)
142
+ column_filters = params[:column_filters] || {}
143
+ # Clean up blank filters
144
+ column_filters.reject! { |_, v| v.blank? }
145
+
142
146
  database_manager.table_records(
143
147
  table_name,
144
148
  @current_page,
145
149
  @order_by,
146
150
  @order_direction,
147
- @per_page
151
+ @per_page,
152
+ column_filters || {}
148
153
  )
149
154
  end
150
155
 
156
+ # Get filtered record count for a table
157
+ def fetch_filtered_record_count(table_name, column_filters)
158
+ database_manager.filtered_record_count(table_name, column_filters)
159
+ end
160
+
151
161
  # Safely quote a table name, with fallback
152
162
  def safe_quote_table_name(table_name)
153
163
  database_manager.connection.quote_table_name(table_name)
@@ -15,7 +15,15 @@ module Dbviewer
15
15
  set_pagination_params
16
16
  set_sorting_params
17
17
 
18
- @total_count = fetch_table_record_count(@table_name)
18
+ # Extract column filters from params
19
+ @column_filters = params[:column_filters].presence ? params[:column_filters].to_enum.to_h : {}
20
+
21
+ if @column_filters.present? && @column_filters.values.any?(&:present?)
22
+ @total_count = fetch_filtered_record_count(@table_name, @column_filters)
23
+ else
24
+ @total_count = fetch_table_record_count(@table_name)
25
+ end
26
+
19
27
  @total_pages = calculate_total_pages(@total_count, @per_page)
20
28
  @records = fetch_table_records(@table_name)
21
29
 
@@ -66,51 +66,73 @@
66
66
  <div class="d-flex align-items-center">
67
67
  <div class="me-3">
68
68
  <label for="per-page-select" class="me-2">Per page:</label>
69
- <select id="per-page-select" class="form-select form-select-sm" onchange="window.location.href='<%= table_path(@table_name) %>?per_page=' + this.value + '&page=1&order_by=<%= @order_by %>&order_direction=<%= @order_direction %>'">
69
+ <select id="per-page-select" class="form-select form-select-sm" onchange="window.location.href='<%= table_path(@table_name) %>?per_page=' + this.value + '&page=1&order_by=<%= @order_by %>&order_direction=<%= @order_direction %><%= @column_filters.reject { |_, v| v.blank? }.any? ? "&" + @column_filters.reject { |_, v| v.blank? }.map { |k, v| "column_filters[#{k}]=#{CGI.escape(v.to_s)}" }.join("&") : "" %>'">
70
70
  <% Dbviewer::TablesController.per_page_options.each do |option| %>
71
71
  <option value="<%= option %>" <%= 'selected' if @per_page == option %>><%= option %></option>
72
72
  <% end %>
73
73
  </select>
74
74
  </div>
75
75
  <span class="badge bg-secondary">Total: <%= @total_count %> records</span>
76
+ <% active_filters = @column_filters.reject { |_, v| v.blank? }.size %>
77
+ <% if active_filters > 0 %>
78
+ <span class="badge bg-info ms-2" title="Active filters"><i class="bi bi-funnel-fill me-1"></i><%= active_filters %></span>
79
+ <% end %>
76
80
  </div>
77
81
  </div>
78
82
  <div class="card-body p-0">
79
83
  <div class="table-responsive dbviewer-scrollable">
80
- <table class="table table-bordered table-striped rounded-none">
81
- <% if @records.present? && @records.columns.any? %>
82
- <thead class="dbviewer-table-header">
83
- <tr>
84
- <% @records.columns.each do |column_name| %>
85
- <th>
86
- <%= column_name %>
87
- </th>
88
- <% end %>
89
- </tr>
90
- </thead>
91
- <tbody>
92
- <% @records.rows.each do |row| %>
84
+ <%= form_with(url: table_path(@table_name), method: :get, local: true, id: "column-filters-form", class: "mb-0") do |form| %>
85
+ <% # Hidden fields to preserve current parameters %>
86
+ <%= form.hidden_field :per_page, value: @per_page %>
87
+ <%= form.hidden_field :order_by, value: @order_by %>
88
+ <%= form.hidden_field :order_direction, value: @order_direction %>
89
+ <%= form.hidden_field :page, value: 1 %> <!-- Reset to first page on filter -->
90
+
91
+ <table class="table table-bordered table-striped rounded-none">
92
+ <thead class="dbviewer-table-header">
93
93
  <tr>
94
- <% row.each do |cell| %>
95
- <% cell_value = format_cell_value(cell) %>
96
- <td title="<%= cell_value %>"><%= cell_value %></td>
94
+ <% @records.columns.each do |column_name| %>
95
+ <th class="pe-4">
96
+ <%= column_name %>
97
+ </th>
97
98
  <% end %>
98
99
  </tr>
99
- <% end %>
100
+ <tr class="column-filters">
101
+ <% @records.columns.each do |column_name| %>
102
+ <th class="p-0">
103
+ <%= form.text_field "column_filters[#{column_name}]",
104
+ value: @column_filters[column_name],
105
+ placeholder: "",
106
+ class: "form-control form-control-sm column-filter rounded-0",
107
+ data: { column: column_name } %>
108
+ </th>
109
+ <% end %>
110
+ </tr>
111
+ </thead>
112
+ <tbody>
113
+ <% if @records.empty? %>
114
+ <tr>
115
+ <td colspan="100%" class="text-center">No records found or table is empty.</td>
116
+ </tr>
117
+ <% end %>
118
+ <% @records.rows.each do |row| %>
119
+ <tr>
120
+ <% row.each do |cell| %>
121
+ <% cell_value = format_cell_value(cell) %>
122
+ <td title="<%= cell_value %>"><%= cell_value %></td>
123
+ <% end %>
124
+ </tr>
125
+ <% end %>
100
126
  </tbody>
101
- <% else %>
102
- <tr>
103
- <td colspan="100%">No records found or table is empty.</td>
104
- </tr>
105
- <% end %>
106
127
  </table>
128
+ <% end %> <!-- End of form_with -->
107
129
  </div>
108
130
 
109
131
  <% if @total_pages > 1 %>
110
132
  <nav aria-label="Page navigation">
111
133
  <ul class="pagination justify-content-center">
112
134
  <li class="page-item <%= 'disabled' if @current_page == 1 %>">
113
- <%= link_to '«', table_path(@table_name, page: [@current_page - 1, 1].max, order_by: @order_by, order_direction: @order_direction, per_page: @per_page), class: 'page-link' %>
135
+ <%= link_to '«', table_path(@table_name, page: [@current_page - 1, 1].max, order_by: @order_by, order_direction: @order_direction, per_page: @per_page, column_filters: @column_filters), class: 'page-link' %>
114
136
  </li>
115
137
 
116
138
  <% start_page = [1, @current_page - 2].max %>
@@ -119,12 +141,12 @@
119
141
 
120
142
  <% (start_page..end_page).each do |page_num| %>
121
143
  <li class="page-item <%= 'active' if page_num == @current_page %>">
122
- <%= link_to page_num, table_path(@table_name, page: page_num, order_by: @order_by, order_direction: @order_direction, per_page: @per_page), class: 'page-link' %>
144
+ <%= link_to page_num, table_path(@table_name, page: page_num, order_by: @order_by, order_direction: @order_direction, per_page: @per_page, column_filters: @column_filters), class: 'page-link' %>
123
145
  </li>
124
146
  <% end %>
125
147
 
126
148
  <li class="page-item <%= 'disabled' if @current_page == @total_pages %>">
127
- <%= link_to '»', table_path(@table_name, page: [@current_page + 1, @total_pages].min, order_by: @order_by, order_direction: @order_direction, per_page: @per_page), class: 'page-link' %>
149
+ <%= link_to '»', table_path(@table_name, page: [@current_page + 1, @total_pages].min, order_by: @order_by, order_direction: @order_direction, per_page: @per_page, column_filters: @column_filters), class: 'page-link' %>
128
150
  </li>
129
151
  </ul>
130
152
  </nav>
@@ -190,6 +212,85 @@
190
212
  </div>
191
213
  </div>
192
214
 
215
+ <script>
216
+ document.addEventListener('DOMContentLoaded', function() {
217
+ // Column filter functionality
218
+ const columnFilters = document.querySelectorAll('.column-filter');
219
+ const filterForm = document.getElementById('column-filters-form');
220
+
221
+ // Add debounce function to reduce form submissions
222
+ function debounce(func, wait) {
223
+ let timeout;
224
+ return function() {
225
+ const context = this;
226
+ const args = arguments;
227
+ clearTimeout(timeout);
228
+ timeout = setTimeout(function() {
229
+ func.apply(context, args);
230
+ }, wait);
231
+ };
232
+ }
233
+
234
+ // Function to submit the form
235
+ const submitForm = debounce(function() {
236
+ filterForm.submit();
237
+ }, 500);
238
+
239
+ // Add event listeners to all filter inputs
240
+ columnFilters.forEach(function(filter) {
241
+ filter.addEventListener('input', submitForm);
242
+ });
243
+
244
+ // Add clear button functionality if there are any filters applied
245
+ const hasActiveFilters = Array.from(columnFilters).some(input => input.value);
246
+
247
+ if (hasActiveFilters) {
248
+ // Add a clear filters button
249
+ const paginationContainer = document.querySelector('nav[aria-label="Page navigation"]') ||
250
+ document.querySelector('.table-responsive');
251
+
252
+ if (paginationContainer) {
253
+ const clearButton = document.createElement('div');
254
+ clearButton.className = 'text-center mt-3';
255
+ clearButton.innerHTML = '<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-filters">' +
256
+ '<i class="bi bi-x-circle me-1"></i>Clear All Filters</button>';
257
+
258
+ paginationContainer.insertAdjacentHTML('afterend', clearButton.outerHTML);
259
+
260
+ document.getElementById('clear-filters').addEventListener('click', function() {
261
+ columnFilters.forEach(filter => filter.value = '');
262
+ submitForm();
263
+ });
264
+ }
265
+ }
266
+ });
267
+ </script>
268
+
269
+ <style>
270
+ /* Column filter styling */
271
+ .column-filters td {
272
+ padding: 0.5rem;
273
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
274
+ }
275
+
276
+ .column-filter {
277
+ width: 100%;
278
+ border: 1px solid rgba(0,0,0,0.1);
279
+ padding: 0.3rem 0.5rem;
280
+ font-size: 0.85rem;
281
+ }
282
+
283
+ [data-bs-theme="dark"] .column-filters td {
284
+ background-color: rgba(255,255,255,0.05);
285
+ }
286
+
287
+ [data-bs-theme="dark"] .column-filter {
288
+ background-color: rgba(255,255,255,0.1);
289
+ color: rgba(255,255,255,0.9);
290
+ border-color: rgba(255,255,255,0.15);
291
+ }
292
+ </style>
293
+
193
294
  <% if @timestamp_data.present? %>
194
295
  <script>
195
296
  document.addEventListener('DOMContentLoaded', function() {
@@ -211,7 +211,8 @@
211
211
  border: 1px solid #495057;
212
212
  }
213
213
 
214
- .dbviewer-scrollable { max-height: 500px; overflow-y: auto; }
214
+ .dbviewer-scrollable { max-height: 700px; overflow-y: auto; }
215
+ .dbviewer-scrollable thead { position: sticky; top: 0; z-index: 1; }
215
216
 
216
217
  /* Badge styling for dark mode */
217
218
  [data-bs-theme="dark"] .bg-secondary-subtle {
@@ -75,9 +75,10 @@ module Dbviewer
75
75
  # @param direction [String] Sort direction ('ASC' or 'DESC')
76
76
  # @param per_page [Integer] Number of records per page
77
77
  # @return [ActiveRecord::Result] Result set with columns and rows
78
- def table_records(table_name, page = 1, order_by = nil, direction = "ASC", per_page = nil)
78
+ def table_records(table_name, page = 1, order_by = nil, direction = "ASC", per_page = nil, column_filters = nil)
79
79
  page = [ 1, page.to_i ].max
80
80
  default_per_page = self.class.default_per_page
81
+ column_filters ||= {}
81
82
  max_records = self.class.max_records
82
83
  per_page = (per_page || default_per_page).to_i
83
84
 
@@ -87,6 +88,32 @@ module Dbviewer
87
88
  model = get_model_for(table_name)
88
89
  query = model.all
89
90
 
91
+ # Apply column filters if provided
92
+ if column_filters.present?
93
+ column_filters.each do |column, value|
94
+ next if value.blank?
95
+ next unless column_exists?(table_name, column)
96
+
97
+ # Use LIKE for string-based searches, = for exact matches on other types
98
+ column_info = table_columns(table_name).find { |c| c[:name] == column }
99
+ if column_info
100
+ column_type = column_info[:type].to_s
101
+
102
+ if column_type =~ /char|text|string|uuid|enum/i
103
+ query = query.where("#{connection.quote_column_name(column)} LIKE ?", "%#{value}%")
104
+ else
105
+ # For numeric types, try exact match if value looks like a number
106
+ if value =~ /\A[+-]?\d+(\.\d+)?\z/
107
+ query = query.where(column => value)
108
+ else
109
+ # Otherwise, try string comparison for non-string fields
110
+ query = query.where("CAST(#{connection.quote_column_name(column)} AS CHAR) LIKE ?", "%#{value}%")
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
90
117
  # Apply sorting if provided
91
118
  if order_by.present? && column_exists?(table_name, order_by)
92
119
  direction = %w[ASC DESC].include?(direction.to_s.upcase) ? direction.to_s.upcase : "ASC"
@@ -110,6 +137,43 @@ module Dbviewer
110
137
  table_count(table_name)
111
138
  end
112
139
 
140
+ # Get the number of records in a table with filters applied
141
+ # @param table_name [String] Name of the table
142
+ # @param column_filters [Hash] Hash of column_name => filter_value for filtering
143
+ # @return [Integer] Number of filtered records
144
+ def filtered_record_count(table_name, column_filters = {})
145
+ model = get_model_for(table_name)
146
+ query = model.all
147
+
148
+ # Apply column filters if provided
149
+ if column_filters.present?
150
+ column_filters.each do |column, value|
151
+ next if value.blank?
152
+ next unless column_exists?(table_name, column)
153
+
154
+ # Use LIKE for string-based searches, = for exact matches on other types
155
+ column_info = table_columns(table_name).find { |c| c[:name] == column }
156
+ if column_info
157
+ column_type = column_info[:type].to_s
158
+
159
+ if column_type =~ /char|text|string|uuid|enum/i
160
+ query = query.where("#{connection.quote_column_name(column)} LIKE ?", "%#{value}%")
161
+ else
162
+ # For numeric types, try exact match if value looks like a number
163
+ if value =~ /\A[+-]?\d+(\.\d+)?\z/
164
+ query = query.where(column => value)
165
+ else
166
+ # Otherwise, try string comparison for non-string fields
167
+ query = query.where("CAST(#{connection.quote_column_name(column)} AS CHAR) LIKE ?", "%#{value}%")
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ query.count
175
+ end
176
+
113
177
  # Get the number of columns in a table
114
178
  # @param table_name [String] Name of the table
115
179
  # @return [Integer] Number of columns
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.3.3"
2
+ VERSION = "0.3.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh