dbviewer 0.7.6 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce8d012a4b4e0bde526cf42fa8a40dd1402a2f787ae06381dbf3b6bfeef1dad1
4
- data.tar.gz: 49a0b962162fadbdbe51feade176aaf51d211126f45295cf6df5b4f4614d019c
3
+ metadata.gz: 3b8c2130ace09b45819bd1fa2d739fecb8a32a75a1348e7c094cb8ce0d66bd71
4
+ data.tar.gz: 65ff2d9bf93a401c7b193dca798e8afdfe4bb535d7b0c4ee5ce7729312792d49
5
5
  SHA512:
6
- metadata.gz: 76520c547bb120caed2093b6138c55f818c6fd9bbbcdff534ea208af050e721b2319179b69710480330783db884d8f1e6bd661ed807c91599717a90811f449fd
7
- data.tar.gz: ba7080bc4598a5e3cf07bede7181ff91e2ab687d43c5583b616f7fa8edc14c1cd871ef9e27764a7ef6cddd94617c4433949328e46f19ec88442d9cc1af2edfbf
6
+ metadata.gz: fdc6983afc3916e25335e0ce9cb33d9525b16dc7c468d24d4a22794c8fb3509d77527a7bd64d48d2e5fd2637eed1b5cea0a3a92d3011a3486b2869d93ca363f9
7
+ data.tar.gz: e3a401344d1fcf3fc2d2ecedc0ac283295ef6c5c364fe396095b53d284af42430ab3f49dda08b6d2c78e0b6a0cc808977b558a06b0aa00287df52ec5b4bb2125
data/README.md CHANGED
@@ -180,7 +180,7 @@ Dbviewer.configure do |config|
180
180
  end
181
181
  ```
182
182
 
183
- Each connection needs to reference an ActiveRecord class that establishes a database connection. For more details, see [Multiple Database Connections](docs/multiple_connections.md).
183
+ Each connection needs to reference an ActiveRecord class that establishes a database connection.
184
184
 
185
185
  ## 🪵 Query Logging (Development Only)
186
186
 
@@ -326,21 +326,21 @@ If you prefer to set up manually:
326
326
  bundle install
327
327
 
328
328
  # Set up the dummy app database
329
- cd test/dummy
329
+ cd sample/app
330
330
  bin/rails db:prepare
331
331
  bin/rails db:migrate
332
332
  bin/rails db:seed
333
333
  cd ../..
334
334
 
335
335
  # Prepare test environment
336
- cd test/dummy && bin/rails db:test:prepare && cd ../..
336
+ cd sample/app && bin/rails db:test:prepare && cd ../..
337
337
  ```
338
338
 
339
339
  ### Development Commands
340
340
 
341
341
  ```bash
342
342
  # Start the development server
343
- cd test/dummy && bin/rails server
343
+ cd sample/app && bin/rails server
344
344
 
345
345
  # Run tests
346
346
  bundle exec rspec
@@ -357,7 +357,7 @@ gem build dbviewer.gemspec
357
357
 
358
358
  ### Testing Your Changes
359
359
 
360
- 1. Start the dummy Rails application: `cd test/dummy && bin/rails server`
360
+ 1. Start the dummy Rails application: `cd sample/app && bin/rails server`
361
361
  2. Visit `http://localhost:3000/dbviewer` to test your changes
362
362
  3. The dummy app includes sample data across multiple tables to test various DBViewer features
363
363
 
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require "bundler/setup"
2
2
 
3
- APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
3
+ APP_RAKEFILE = File.expand_path("sample/app/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
6
  load "rails/tasks/statistics.rake"
@@ -9,6 +9,15 @@ document.addEventListener("DOMContentLoaded", function () {
9
9
  return;
10
10
  }
11
11
 
12
+ // Helper function to debounce rapid function calls
13
+ function debounce(func, wait) {
14
+ let timeout;
15
+ return function (...args) {
16
+ clearTimeout(timeout);
17
+ timeout = setTimeout(() => func.apply(this, args), wait);
18
+ };
19
+ }
20
+
12
21
  // Initialize mermaid with theme detection like mini ERD
13
22
  mermaid.initialize({
14
23
  startOnLoad: true,
@@ -102,6 +111,7 @@ document.addEventListener("DOMContentLoaded", function () {
102
111
  // Function to fetch relationships asynchronously
103
112
  async function fetchRelationships() {
104
113
  const apiPath = document.getElementById("relationships_api_path").value;
114
+ updateRelationshipsStatus(false, "Requesting relationships data...");
105
115
  try {
106
116
  const response = await fetch(apiPath, {
107
117
  headers: {
@@ -114,15 +124,19 @@ document.addEventListener("DOMContentLoaded", function () {
114
124
  throw new Error(`HTTP error! status: ${response.status}`);
115
125
  }
116
126
 
127
+ updateRelationshipsStatus(false, "Processing relationships data...");
117
128
  const data = await response.json();
118
129
  relationships = data.relationships || [];
119
130
  relationshipsLoaded = true;
120
- updateRelationshipsStatus(true);
131
+ updateRelationshipsStatus(
132
+ true,
133
+ `Loaded ${relationships.length} relationships`
134
+ );
121
135
  return relationships;
122
136
  } catch (error) {
123
137
  console.error("Error fetching relationships:", error);
124
138
  relationshipsLoaded = true; // Mark as loaded even on error to prevent infinite loading
125
- updateRelationshipsStatus(true);
139
+ updateRelationshipsStatus(true, "Failed to load relationships", true);
126
140
  return [];
127
141
  }
128
142
  }
@@ -155,20 +169,31 @@ document.addEventListener("DOMContentLoaded", function () {
155
169
  }
156
170
 
157
171
  // Function to update relationships status
158
- function updateRelationshipsStatus(loaded) {
172
+ function updateRelationshipsStatus(loaded, message, isError = false) {
159
173
  const relationshipsStatus = document.getElementById("relationships-status");
160
174
  if (relationshipsStatus) {
161
- if (loaded) {
175
+ if (loaded && !isError) {
162
176
  relationshipsStatus.innerHTML = `
163
177
  <i class="bi bi-check-circle text-success me-2"></i>
164
- <small class="text-success">Relationships loaded</small>
178
+ <small class="text-success">${
179
+ message || "Relationships loaded"
180
+ }</small>
181
+ `;
182
+ } else if (loaded && isError) {
183
+ relationshipsStatus.innerHTML = `
184
+ <i class="bi bi-exclamation-triangle text-warning me-2"></i>
185
+ <small class="text-warning">${
186
+ message || "Error loading relationships"
187
+ }</small>
165
188
  `;
166
189
  } else {
167
190
  relationshipsStatus.innerHTML = `
168
191
  <div class="spinner-border spinner-border-sm text-secondary me-2" role="status">
169
192
  <span class="visually-hidden">Loading...</span>
170
193
  </div>
171
- <small class="text-muted">Loading relationships...</small>
194
+ <small class="text-muted">${
195
+ message || "Loading relationships..."
196
+ }</small>
172
197
  `;
173
198
  }
174
199
  }
@@ -189,10 +214,23 @@ document.addEventListener("DOMContentLoaded", function () {
189
214
  updateLoadingStatus("Loading table details...");
190
215
 
191
216
  // Start fetching relationships immediately
192
- updateRelationshipsStatus(false);
217
+ updateRelationshipsStatus(false, "Loading relationships...");
193
218
  const relationshipsPromise = fetchRelationships();
194
219
  const tablePath = document.getElementById("tables_path").value;
195
220
 
221
+ // Function to fetch tables in batches for better performance
222
+ async function fetchTablesInBatches(tables, batchSize = 5) {
223
+ const batches = [];
224
+ for (let i = 0; i < tables.length; i += batchSize) {
225
+ batches.push(tables.slice(i, i + batchSize));
226
+ }
227
+
228
+ for (const batch of batches) {
229
+ await Promise.all(batch.map((table) => fetchTableColumns(table.name)));
230
+ // This creates a visual effect of tables loading in batches
231
+ }
232
+ }
233
+
196
234
  // First pass: add all tables with minimal info and start loading columns
197
235
  // Function to fetch column data for a table
198
236
  async function fetchTableColumns(tableName) {
@@ -204,6 +242,12 @@ document.addEventListener("DOMContentLoaded", function () {
204
242
  },
205
243
  });
206
244
 
245
+ if (!response.ok) {
246
+ throw new Error(
247
+ `Failed to fetch table ${tableName}: ${response.status}`
248
+ );
249
+ }
250
+
207
251
  const data = await response.json();
208
252
 
209
253
  if (data && data.columns) {
@@ -217,22 +261,29 @@ document.addEventListener("DOMContentLoaded", function () {
217
261
  }
218
262
  } catch (error) {
219
263
  console.error(`Error fetching columns for table ${tableName}:`, error);
264
+ // Add better error handling
265
+ showError(
266
+ "Table Loading Error",
267
+ `Failed to load columns for table ${tableName}`,
268
+ error.message
269
+ );
220
270
  columnsLoadedCount++;
221
271
  updateTableProgress(columnsLoadedCount, totalTables);
222
272
  checkIfReadyToUpdate();
223
273
  }
224
274
  }
225
275
 
276
+ // Generate initial table representation
226
277
  tables.forEach(function (table) {
227
278
  const tableName = table.name;
228
279
  mermaidDefinition += ` ${tableName} {\n`;
229
280
  mermaidDefinition += ` string id\n`;
230
281
  mermaidDefinition += " }\n";
231
-
232
- // Start loading column data asynchronously
233
- fetchTableColumns(tableName);
234
282
  });
235
283
 
284
+ // Start loading column data asynchronously in batches
285
+ fetchTablesInBatches(tables);
286
+
236
287
  // Function to check if we're ready to update the diagram with full data
237
288
  function checkIfReadyToUpdate() {
238
289
  if (columnsLoadedCount === totalTables && relationshipsLoaded) {
@@ -381,18 +432,54 @@ document.addEventListener("DOMContentLoaded", function () {
381
432
  panZoomInstance.zoom(1);
382
433
 
383
434
  // Add event listeners for zoom controls
384
- document.getElementById("zoomIn").addEventListener("click", function () {
385
- panZoomInstance.zoomIn();
386
- });
435
+ document.getElementById("zoomIn").addEventListener(
436
+ "click",
437
+ debounce(function () {
438
+ panZoomInstance.zoomIn();
439
+ }, 100)
440
+ );
387
441
 
388
- document.getElementById("zoomOut").addEventListener("click", function () {
389
- panZoomInstance.zoomOut();
390
- });
442
+ document.getElementById("zoomOut").addEventListener(
443
+ "click",
444
+ debounce(function () {
445
+ panZoomInstance.zoomOut();
446
+ }, 100)
447
+ );
448
+
449
+ document.getElementById("resetView").addEventListener(
450
+ "click",
451
+ debounce(function () {
452
+ panZoomInstance.reset();
453
+ }, 100)
454
+ );
391
455
 
392
- document.getElementById("resetView").addEventListener("click", function () {
393
- panZoomInstance.reset();
456
+ // Add keyboard shortcuts for zoom controls
457
+ document.addEventListener("keydown", (e) => {
458
+ if (e.ctrlKey || e.metaKey) {
459
+ if (e.key === "+" || e.key === "=") {
460
+ e.preventDefault();
461
+ panZoomInstance.zoomIn();
462
+ } else if (e.key === "-") {
463
+ e.preventDefault();
464
+ panZoomInstance.zoomOut();
465
+ } else if (e.key === "0") {
466
+ e.preventDefault();
467
+ panZoomInstance.reset();
468
+ }
469
+ }
394
470
  });
395
471
 
472
+ // Improve ARIA attributes
473
+ document
474
+ .getElementById("zoomIn")
475
+ .setAttribute("aria-label", "Zoom in diagram");
476
+ document
477
+ .getElementById("zoomOut")
478
+ .setAttribute("aria-label", "Zoom out diagram");
479
+ document
480
+ .getElementById("resetView")
481
+ .setAttribute("aria-label", "Reset diagram view");
482
+
396
483
  // Update initial percentage display
397
484
  const zoomDisplay = document.getElementById("zoomPercentage");
398
485
  if (zoomDisplay) {
@@ -447,12 +534,18 @@ document.addEventListener("DOMContentLoaded", function () {
447
534
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
448
535
 
449
536
  // Create download link and trigger download
537
+ const objectURL = URL.createObjectURL(blob);
450
538
  const downloadLink = document.createElement("a");
451
- downloadLink.href = URL.createObjectURL(blob);
539
+ downloadLink.href = objectURL;
452
540
  downloadLink.download = `database_erd_${timestamp}.svg`;
453
541
  document.body.appendChild(downloadLink);
454
542
  downloadLink.click();
455
543
  document.body.removeChild(downloadLink);
544
+
545
+ // Clean up object URL
546
+ setTimeout(() => {
547
+ URL.revokeObjectURL(objectURL);
548
+ }, 100);
456
549
  } catch (error) {
457
550
  console.error("Error downloading SVG:", error);
458
551
  alert("Error downloading SVG. Please check console for details.");
@@ -520,12 +613,18 @@ document.addEventListener("DOMContentLoaded", function () {
520
613
 
521
614
  // Convert canvas to PNG and trigger download
522
615
  canvas.toBlob(function (blob) {
616
+ const objectURL = URL.createObjectURL(blob);
523
617
  const downloadLink = document.createElement("a");
524
- downloadLink.href = URL.createObjectURL(blob);
618
+ downloadLink.href = objectURL;
525
619
  downloadLink.download = `database_erd_${timestamp}.png`;
526
620
  document.body.appendChild(downloadLink);
527
621
  downloadLink.click();
528
622
  document.body.removeChild(downloadLink);
623
+
624
+ // Clean up object URL
625
+ setTimeout(() => {
626
+ URL.revokeObjectURL(objectURL);
627
+ }, 100);
529
628
  }, "image/png");
530
629
 
531
630
  // Clean up
@@ -554,4 +653,38 @@ document.addEventListener("DOMContentLoaded", function () {
554
653
  e.preventDefault();
555
654
  downloadAsPNG();
556
655
  });
656
+
657
+ // Add theme observer to update diagram when theme changes
658
+ function setupThemeObserver() {
659
+ const observer = new MutationObserver((mutations) => {
660
+ mutations.forEach((mutation) => {
661
+ if (mutation.attributeName === "data-bs-theme") {
662
+ const newTheme =
663
+ document.documentElement.getAttribute("data-bs-theme");
664
+ mermaid.initialize({
665
+ theme: newTheme === "dark" ? "dark" : "default",
666
+ // Keep other settings
667
+ securityLevel: "loose",
668
+ er: {
669
+ diagramPadding: 20,
670
+ layoutDirection: "TB",
671
+ minEntityWidth: 100,
672
+ minEntityHeight: 75,
673
+ entityPadding: 15,
674
+ stroke: "gray",
675
+ fill: "honeydew",
676
+ fontSize: 20,
677
+ },
678
+ });
679
+ // Trigger redraw if diagram is already displayed
680
+ if (diagramReady) {
681
+ updateDiagramWithFullData();
682
+ }
683
+ }
684
+ });
685
+ });
686
+ observer.observe(document.documentElement, { attributes: true });
687
+ }
688
+
689
+ setupThemeObserver();
557
690
  });
@@ -13,16 +13,12 @@ module Dbviewer
13
13
  def fetch_recent_queries
14
14
  return [] unless query_logging_enabled?
15
15
 
16
- Dbviewer::Query::Logger.instance.recent_queries(limit: queries_limit)
16
+ Dbviewer::Query::Logger.instance.recent_queries(limit: 10)
17
17
  end
18
18
 
19
19
  def query_logging_enabled?
20
20
  Dbviewer.configuration.enable_query_logging
21
21
  end
22
-
23
- def queries_limit
24
- 10
25
- end
26
22
  end
27
23
  end
28
24
  end
@@ -57,12 +57,7 @@ module Dbviewer
57
57
  @tables = fetch_tables # Fetch tables for sidebar
58
58
 
59
59
  @query = prepare_query(@table_name, params[:query])
60
- @records = begin
61
- execute_query(@query)
62
- rescue => e
63
- @error = "Error executing query: #{e.message}"
64
- nil
65
- end
60
+ @records = execute_query(@query)
66
61
 
67
62
  render :query
68
63
  end
@@ -2,11 +2,13 @@ module Dbviewer
2
2
  module ApplicationHelper
3
3
  # Include all the helper modules organized by logical concerns
4
4
  include DatabaseHelper
5
- include FilterHelper
6
5
  include FormattingHelper
7
- include PaginationHelper
8
- include SortingHelper
9
- include TableRenderingHelper
6
+
7
+ include DatatableUiHelper
8
+ include DatatableUiFilterHelper
9
+ include DatatableUiPaginationHelper
10
+ include DatatableUiSortingHelper
11
+ include DatatableUiTableHelper
10
12
  include NavigationHelper
11
13
  include UiHelper
12
14
  end
@@ -1,5 +1,10 @@
1
1
  module Dbviewer
2
2
  module DatabaseHelper
3
+ # Helper to access the database manager
4
+ def get_database_manager
5
+ @database_manager ||= ::Dbviewer::Database::Manager.new
6
+ end
7
+
3
8
  # Check if a table has a created_at column
4
9
  def has_timestamp_column?(table_name)
5
10
  return false unless table_name.present?
@@ -9,11 +14,6 @@ module Dbviewer
9
14
  columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
10
15
  end
11
16
 
12
- # Helper to access the database manager
13
- def get_database_manager
14
- @database_manager ||= ::Dbviewer::Database::Manager.new
15
- end
16
-
17
17
  # Extract column type from columns info
18
18
  def column_type_from_info(column_name, columns)
19
19
  return nil unless columns.present?
@@ -21,39 +21,5 @@ module Dbviewer
21
21
  column_info = columns.find { |c| c[:name].to_s == column_name.to_s }
22
22
  column_info ? column_info[:type].to_s.downcase : nil
23
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
24
  end
59
25
  end
@@ -1,5 +1,5 @@
1
1
  module Dbviewer
2
- module FilterHelper
2
+ module DatatableUiFilterHelper
3
3
  # Determine default operator based on column type
4
4
  def default_operator_for_column_type(column_type)
5
5
  if column_type && column_type =~ /char|text|string|uuid|enum/i
@@ -0,0 +1,37 @@
1
+ module Dbviewer
2
+ module DatatableUiHelper
3
+ # Get appropriate icon for column data type
4
+ def column_type_icon(column_type)
5
+ case column_type.to_s.downcase
6
+ when /int/, /serial/, /number/, /decimal/, /float/, /double/
7
+ "bi-123"
8
+ when /char/, /text/, /string/, /uuid/
9
+ "bi-fonts"
10
+ when /date/, /time/
11
+ "bi-calendar"
12
+ when /bool/
13
+ "bi-toggle-on"
14
+ when /json/, /jsonb/
15
+ "bi-braces"
16
+ when /array/
17
+ "bi-list-ol"
18
+ else
19
+ "bi-file-earmark"
20
+ end
21
+ end
22
+
23
+ # Format table name for display - truncate if too long
24
+ def format_table_name(table_name)
25
+ if table_name.length > 20
26
+ "#{table_name.first(17)}..."
27
+ else
28
+ table_name
29
+ end
30
+ end
31
+
32
+ # Determine if the current table should be active in the sidebar
33
+ def current_table?(table_name)
34
+ @table_name.present? && @table_name == table_name
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  module Dbviewer
2
- module PaginationHelper
2
+ module DatatableUiPaginationHelper
3
3
  # Common parameters for pagination and filtering
4
4
  def common_params(options = {})
5
5
  params = {
@@ -1,5 +1,5 @@
1
1
  module Dbviewer
2
- module SortingHelper
2
+ module DatatableUiSortingHelper
3
3
  # Returns a sort icon based on the current sort direction
4
4
  def sort_icon(column_name, current_order_by, current_direction)
5
5
  if column_name == current_order_by
@@ -1,5 +1,5 @@
1
1
  module Dbviewer
2
- module TableRenderingHelper
2
+ module DatatableUiTableHelper
3
3
  # Render a complete table header row with sortable columns
4
4
  def render_sortable_header_row(records, order_by, order_direction, table_name, current_page, per_page, column_filters)
5
5
  return content_tag(:tr) { content_tag(:th, "No columns available") } unless records&.columns
@@ -2,29 +2,56 @@ module Dbviewer
2
2
  module FormattingHelper
3
3
  def format_cell_value(value)
4
4
  return "NULL" if value.nil?
5
- return value.to_s.truncate(100) unless value.is_a?(String)
5
+ return format_default_value(value) unless value.is_a?(String)
6
6
 
7
+ format_string_value(value)
8
+ end
9
+
10
+ private
11
+
12
+ def format_string_value(value)
7
13
  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
14
+ when ->(v) { datetime_string?(v) }
15
+ format_datetime_value(value)
16
+ when ->(v) { date_string?(v) }
17
+ format_date_value(value)
18
+ when ->(v) { json_string?(v) }
19
+ format_json_value(value)
25
20
  else
26
- value.to_s.truncate(100)
21
+ format_default_value(value)
27
22
  end
28
23
  end
24
+
25
+ def datetime_string?(value)
26
+ value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
27
+ end
28
+
29
+ def date_string?(value)
30
+ value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
31
+ end
32
+
33
+ def json_string?(value)
34
+ value.match?(/\A{.+}\z/) || value.match?(/\A\[.+\]\z/)
35
+ end
36
+
37
+ def format_datetime_value(value)
38
+ Time.parse(value).strftime("%Y-%m-%d %H:%M:%S")
39
+ rescue
40
+ format_default_value(value)
41
+ end
42
+
43
+ def format_date_value(value)
44
+ value
45
+ end
46
+
47
+ def format_json_value(value)
48
+ JSON.pretty_generate(JSON.parse(value)).truncate(100)
49
+ rescue
50
+ format_default_value(value)
51
+ end
52
+
53
+ def format_default_value(value)
54
+ value.to_s.truncate(100)
55
+ end
29
56
  end
30
57
  end
@@ -115,44 +115,44 @@
115
115
  </div>
116
116
  <div class="offcanvas-body bg-body-tertiary">
117
117
  <ul class="navbar-nav mb-2 mb-lg-0 fw-medium">
118
- <li class="nav-item py-1">
119
- <%= link_to raw('<i class="bi bi-table me-2 text-primary"></i> Tables'), dbviewer.tables_path, class: "nav-link rounded #{tables_nav_class}" %>
120
- </li>
121
- <li class="nav-item py-1">
122
- <%= link_to raw('<i class="bi bi-diagram-3 me-2 text-primary"></i> ERD'), dbviewer.entity_relationship_diagrams_path, class: "nav-link rounded #{erd_nav_class}" %>
123
- </li>
124
- <% if Dbviewer.configuration.enable_query_logging %>
125
- <li class="nav-item py-1">
126
- <%= link_to raw('<i class="bi bi-journal-code me-2 text-primary"></i> SQL Logs'), dbviewer.logs_path, class: "nav-link rounded #{logs_nav_class}" %>
127
- </li>
128
- <% end %>
129
- <li class="nav-item dropdown py-1">
130
- <a class="nav-link dropdown-toggle d-flex align-items-center rounded" href="#" id="offcanvasDbDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
131
- <i class="bi bi-database me-2 text-primary"></i> <%= (current_conn = available_connections.find { |c| c[:current] }) ? current_conn[:name] : "Database" %>
132
- </a>
133
- <ul class="dropdown-menu shadow-sm mt-2" aria-labelledby="offcanvasDbDropdown">
134
- <% available_connections.each do |connection| %>
135
- <li>
136
- <%= button_to connection_path(connection[:key]), method: :post, class: "dropdown-item border-0 w-100 text-start #{'active' if connection[:current]}" do %>
137
- <% if connection[:current] %>
138
- <i class="bi bi-check2-circle me-2 text-primary"></i>
139
- <% else %>
140
- <i class="bi bi-circle me-2"></i>
141
- <% end %>
142
- <%= connection[:name] %>
143
- <% end %>
144
- </li>
118
+ <li class="nav-item py-1">
119
+ <%= link_to raw('<i class="bi bi-table me-2 text-primary"></i> Tables'), dbviewer.tables_path, class: "nav-link rounded #{tables_nav_class}" %>
120
+ </li>
121
+ <li class="nav-item py-1">
122
+ <%= link_to raw('<i class="bi bi-diagram-3 me-2 text-primary"></i> ERD'), dbviewer.entity_relationship_diagrams_path, class: "nav-link rounded #{erd_nav_class}" %>
123
+ </li>
124
+ <% if Dbviewer.configuration.enable_query_logging %>
125
+ <li class="nav-item py-1">
126
+ <%= link_to raw('<i class="bi bi-journal-code me-2 text-primary"></i> SQL Logs'), dbviewer.logs_path, class: "nav-link rounded #{logs_nav_class}" %>
127
+ </li>
145
128
  <% end %>
146
- <li><hr class="dropdown-divider"></li>
147
- <li><%= link_to "<i class='bi bi-gear me-2'></i> Manage Connections".html_safe, connections_path, class: "dropdown-item" %></li>
148
- </ul>
149
- </li>
150
- <li class="mt-4 pt-2 border-top">
151
- <div class="d-flex align-items-center py-2">
152
- <i class="bi bi-tools me-2 text-secondary"></i>
153
- <span class="text-secondary"><%= Rails.env %> environment</span>
154
- </div>
155
- </li>
129
+ <li class="nav-item dropdown py-1">
130
+ <a class="nav-link dropdown-toggle d-flex align-items-center rounded" href="#" id="offcanvasDbDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
131
+ <i class="bi bi-database me-2 text-primary"></i> <%= (current_conn = available_connections.find { |c| c[:current] }) ? current_conn[:name] : "Database" %>
132
+ </a>
133
+ <ul class="dropdown-menu shadow-sm mt-2" aria-labelledby="offcanvasDbDropdown">
134
+ <% available_connections.each do |connection| %>
135
+ <li>
136
+ <%= button_to connection_path(connection[:key]), method: :post, class: "dropdown-item border-0 w-100 text-start #{'active' if connection[:current]}" do %>
137
+ <% if connection[:current] %>
138
+ <i class="bi bi-check2-circle me-2 text-primary"></i>
139
+ <% else %>
140
+ <i class="bi bi-circle me-2"></i>
141
+ <% end %>
142
+ <%= connection[:name] %>
143
+ <% end %>
144
+ </li>
145
+ <% end %>
146
+ <li><hr class="dropdown-divider"></li>
147
+ <li><%= link_to "<i class='bi bi-gear me-2'></i> Manage Connections".html_safe, connections_path, class: "dropdown-item" %></li>
148
+ </ul>
149
+ </li>
150
+ <li class="mt-4 pt-2 border-top">
151
+ <div class="d-flex align-items-center py-2">
152
+ <i class="bi bi-tools me-2 text-secondary"></i>
153
+ <span class="text-secondary"><%= Rails.env %> environment</span>
154
+ </div>
155
+ </li>
156
156
  </ul>
157
157
  </div>
158
158
  </div>
@@ -18,24 +18,7 @@
18
18
  <%= link_to table_path(table[:name], table_url_params), title: table[:name],
19
19
  class: "list-group-item list-group-item-action d-flex align-items-center #{'active' if current_table?(table[:name])}",
20
20
  tabindex: "0",
21
- data: { table_name: table[:name] },
22
- onkeydown: "
23
- if(event.key === 'ArrowDown') {
24
- event.preventDefault();
25
- let next = this.nextElementSibling;
26
- while(next && next.classList.contains('d-none')) {
27
- next = next.nextElementSibling;
28
- }
29
- if(next) next.focus();
30
- } else if(event.key === 'ArrowUp') {
31
- event.preventDefault();
32
- let prev = this.previousElementSibling;
33
- while(prev && prev.classList.contains('d-none')) {
34
- prev = prev.previousElementSibling;
35
- }
36
- if(prev) prev.focus();
37
- else document.getElementById('tableSearch')?.focus();
38
- }" do %>
21
+ data: { table_name: table[:name] } do %>
39
22
  <div class="text-truncate">
40
23
  <i class="bi bi-table me-2"></i>
41
24
  <%= format_table_name(table[:name]) %>
@@ -15,28 +15,7 @@ module Dbviewer
15
15
  # @return [ActiveRecord::Result] Result set with columns and rows
16
16
  # @raise [StandardError] If the query is invalid or unsafe
17
17
  def execute_query(sql)
18
- # Validate and normalize the SQL
19
- normalized_sql = ::Dbviewer::Validator::Sql.validate!(sql.to_s)
20
-
21
- # Get max records from configuration
22
- max_records = @config.max_records || 10000
23
-
24
- # Add a safety limit if not already present
25
- unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
26
- normalized_sql = "#{normalized_sql} LIMIT #{max_records}"
27
- end
28
-
29
- # Log and execute the query
30
- Rails.logger.debug("[DBViewer] Executing SQL query: #{normalized_sql}")
31
- start_time = Time.now
32
- result = @connection.exec_query(normalized_sql)
33
- duration = Time.now - start_time
34
-
35
- Rails.logger.debug("[DBViewer] Query completed in #{duration.round(2)}s, returned #{result.rows.size} rows")
36
- result
37
- rescue => e
38
- Rails.logger.error("[DBViewer] SQL query error: #{e.message} for query: #{sql}")
39
- raise e
18
+ exec_query(normalize_sql(sql))
40
19
  end
41
20
 
42
21
  # Execute a SQLite PRAGMA command without adding a LIMIT clause
@@ -44,14 +23,20 @@ module Dbviewer
44
23
  # @return [ActiveRecord::Result] Result set with the PRAGMA value
45
24
  # @raise [StandardError] If the query is invalid or cannot be executed
46
25
  def execute_sqlite_pragma(pragma)
47
- sql = "PRAGMA #{pragma}"
48
- Rails.logger.debug("[DBViewer] Executing SQLite pragma: #{sql}")
49
- result = @connection.exec_query(sql)
50
- Rails.logger.debug("[DBViewer] Pragma completed, returned #{result.rows.size} rows")
51
- result
52
- rescue => e
53
- Rails.logger.error("[DBViewer] SQLite pragma error: #{e.message} for pragma: #{pragma}")
54
- raise e
26
+ exec_query("PRAGMA #{pragma}")
27
+ end
28
+
29
+ private
30
+
31
+ def exec_query(sql)
32
+ @connection.exec_query(sql)
33
+ end
34
+
35
+ def normalize_sql(sql)
36
+ normalized_sql = ::Dbviewer::Validator::Sql.validate!(sql.to_s)
37
+ max_records = @config.max_records || 10000
38
+ normalized_sql = "#{normalized_sql} LIMIT #{max_records}" unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
39
+ normalized_sql
55
40
  end
56
41
  end
57
42
  end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.7.6"
2
+ VERSION = "0.7.7"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.6
4
+ version: 0.7.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-14 00:00:00.000000000 Z
11
+ date: 2025-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -99,12 +99,13 @@ files:
99
99
  - app/controllers/dbviewer/tables_controller.rb
100
100
  - app/helpers/dbviewer/application_helper.rb
101
101
  - app/helpers/dbviewer/database_helper.rb
102
- - app/helpers/dbviewer/filter_helper.rb
102
+ - app/helpers/dbviewer/datatable_ui_filter_helper.rb
103
+ - app/helpers/dbviewer/datatable_ui_helper.rb
104
+ - app/helpers/dbviewer/datatable_ui_pagination_helper.rb
105
+ - app/helpers/dbviewer/datatable_ui_sorting_helper.rb
106
+ - app/helpers/dbviewer/datatable_ui_table_helper.rb
103
107
  - app/helpers/dbviewer/formatting_helper.rb
104
108
  - app/helpers/dbviewer/navigation_helper.rb
105
- - app/helpers/dbviewer/pagination_helper.rb
106
- - app/helpers/dbviewer/sorting_helper.rb
107
- - app/helpers/dbviewer/table_rendering_helper.rb
108
109
  - app/helpers/dbviewer/ui_helper.rb
109
110
  - app/jobs/dbviewer/application_job.rb
110
111
  - app/mailers/dbviewer/application_mailer.rb