dbviewer 0.5.7 → 0.6.0

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.
@@ -0,0 +1,119 @@
1
+ <% content_for :title, "Database Connections" %>
2
+
3
+ <div class="container-fluid">
4
+ <div class="d-flex justify-content-between align-items-center mb-4">
5
+ <h1><i class="bi bi-database-fill me-2"></i> Database Connections</h1>
6
+ </div>
7
+
8
+ <div class="row">
9
+ <div class="col-12">
10
+ <div class="alert alert-info">
11
+ <i class="bi bi-info-circle me-2"></i>
12
+ You can switch between multiple database connections to view different databases in your application.
13
+ </div>
14
+ </div>
15
+ </div>
16
+
17
+ <% if flash[:alert] %>
18
+ <div class="row">
19
+ <div class="col-12">
20
+ <div class="alert alert-danger alert-dismissible fade show" role="alert">
21
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
22
+ <%= flash[:alert] %>
23
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <% end %>
28
+
29
+ <% if flash[:notice] %>
30
+ <div class="row">
31
+ <div class="col-12">
32
+ <div class="alert alert-success alert-dismissible fade show" role="alert">
33
+ <i class="bi bi-check-circle-fill me-2"></i>
34
+ <%= flash[:notice] %>
35
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ <% end %>
40
+
41
+ <div class="row">
42
+ <% @connections.each do |connection| %>
43
+ <div class="col-md-6 col-lg-4 mb-4">
44
+ <div class="card dbviewer-card h-100 <%= 'border-primary' if connection[:current] %>">
45
+ <div class="card-header d-flex justify-content-between align-items-center">
46
+ <h5 class="card-title mb-0">
47
+ <% if connection[:adapter_name]&.downcase&.include?('sqlite') %>
48
+ <i class="bi bi-database-fill me-2 text-success"></i>
49
+ <% elsif connection[:adapter_name]&.downcase&.include?('mysql') %>
50
+ <i class="bi bi-database-fill me-2 text-warning"></i>
51
+ <% elsif connection[:adapter_name]&.downcase&.include?('postgres') %>
52
+ <i class="bi bi-database-fill me-2 text-info"></i>
53
+ <% else %>
54
+ <i class="bi bi-database me-2"></i>
55
+ <% end %>
56
+ <%= connection[:name] %>
57
+ </h5>
58
+ <% if connection[:current] %>
59
+ <span class="badge bg-success">Current</span>
60
+ <% end %>
61
+ </div>
62
+ <div class="card-body">
63
+ <% if connection[:current] %>
64
+ <p class="mb-3"><em>Currently active connection</em></p>
65
+ <% end %>
66
+ <p><strong>Key:</strong> <%= connection[:key] %></p>
67
+ <% if connection[:adapter_name] %>
68
+ <p><strong>Adapter:</strong> <%= connection[:adapter_name] %></p>
69
+ <% end %>
70
+
71
+ <div class="d-flex flex-column mt-3">
72
+ <div class="d-flex justify-content-between mb-2">
73
+ <% if connection[:current] %>
74
+ <button class="btn btn-outline-secondary btn-sm" disabled>
75
+ <i class="bi bi-check-circle-fill me-1"></i> Currently Active
76
+ </button>
77
+ <% else %>
78
+ <%= button_to connection_path(connection[:key]), method: :post, class: "btn btn-primary btn-sm" do %>
79
+ <i class="bi bi-lightning-charge me-1"></i> Switch to this Connection
80
+ <% end %>
81
+ <% end %>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ <% end %>
88
+ </div>
89
+ </div>
90
+
91
+ <script>
92
+ // Form validation script
93
+ document.addEventListener('DOMContentLoaded', function() {
94
+ const forms = document.querySelectorAll('.needs-validation')
95
+
96
+ // Loop over them and prevent submission
97
+ Array.from(forms).forEach(form => {
98
+ form.addEventListener('submit', event => {
99
+ if (!form.checkValidity()) {
100
+ event.preventDefault()
101
+ event.stopPropagation()
102
+ }
103
+
104
+ form.classList.add('was-validated')
105
+ }, false)
106
+ })
107
+
108
+ // Auto-generate a key from the name
109
+ const nameInput = document.getElementById('connection_name')
110
+ const keyInput = document.getElementById('connection_key')
111
+
112
+ nameInput.addEventListener('input', function() {
113
+ keyInput.value = this.value
114
+ .toLowerCase()
115
+ .replace(/\s+/g, '_')
116
+ .replace(/[^a-z0-9_]/g, '')
117
+ })
118
+ })
119
+ </script>
@@ -0,0 +1,79 @@
1
+ <% content_for :title, "Add New Database Connection" %>
2
+
3
+ <div class="container-fluid">
4
+ <div class="d-flex justify-content-between align-items-center mb-4">
5
+ <h1><i class="bi bi-plus-circle me-2"></i> Add New Database Connection</h1>
6
+ </div>
7
+
8
+ <div class="row">
9
+ <div class="col-12 col-md-8 col-lg-6 mx-auto">
10
+ <div class="card dbviewer-card">
11
+ <div class="card-header">
12
+ <h5 class="card-title mb-0">Connection Details</h5>
13
+ </div>
14
+ <div class="card-body">
15
+ <%= form_tag connections_path, method: :post, class: "needs-validation", novalidate: true do %>
16
+ <div class="mb-3">
17
+ <label for="connection_name" class="form-label">Connection Name*</label>
18
+ <input type="text" class="form-control" id="connection_name" name="connection_name"
19
+ placeholder="e.g. Blog Database" required>
20
+ <div class="form-text">A human-readable name for this connection</div>
21
+ </div>
22
+
23
+ <div class="mb-3">
24
+ <label for="connection_key" class="form-label">Connection Key*</label>
25
+ <input type="text" class="form-control" id="connection_key" name="connection_key"
26
+ placeholder="e.g. blog_db" required>
27
+ <div class="form-text">A unique identifier for this connection (lowercase, no spaces)</div>
28
+ </div>
29
+
30
+ <div class="mb-3">
31
+ <label for="connection_class" class="form-label">Connection Class*</label>
32
+ <input type="text" class="form-control" id="connection_class" name="connection_class"
33
+ placeholder="e.g. BlogDatabase" required>
34
+ <div class="form-text">
35
+ The fully qualified class name that establishes the connection.
36
+ This class must inherit from ActiveRecord::Base.
37
+ </div>
38
+ </div>
39
+
40
+ <div class="d-flex justify-content-between mt-4">
41
+ <%= link_to "Cancel", connections_path, class: "btn btn-secondary" %>
42
+ <button type="submit" class="btn btn-primary">Add Connection</button>
43
+ </div>
44
+ <% end %>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <script>
52
+ // Form validation script
53
+ document.addEventListener('DOMContentLoaded', function() {
54
+ const forms = document.querySelectorAll('.needs-validation')
55
+
56
+ // Loop over them and prevent submission
57
+ Array.from(forms).forEach(form => {
58
+ form.addEventListener('submit', event => {
59
+ if (!form.checkValidity()) {
60
+ event.preventDefault()
61
+ event.stopPropagation()
62
+ }
63
+
64
+ form.classList.add('was-validated')
65
+ }, false)
66
+ })
67
+
68
+ // Auto-generate a key from the name
69
+ const nameInput = document.getElementById('connection_name')
70
+ const keyInput = document.getElementById('connection_key')
71
+
72
+ nameInput.addEventListener('input', function() {
73
+ keyInput.value = this.value
74
+ .toLowerCase()
75
+ .replace(/\s+/g, '_')
76
+ .replace(/[^a-z0-9_]/g, '')
77
+ })
78
+ })
79
+ </script>
@@ -0,0 +1,49 @@
1
+ <%# Table list for sidebar %>
2
+ <% if tables.any? %>
3
+ <% tables.each do |table| %>
4
+ <%
5
+ # Build table URL with creation filter params if they exist
6
+ table_url_params = {}
7
+ table_url_params[:creation_filter_start] = @creation_filter_start if defined?(@creation_filter_start) && @creation_filter_start.present?
8
+ table_url_params[:creation_filter_end] = @creation_filter_end if defined?(@creation_filter_end) && @creation_filter_end.present?
9
+ %>
10
+ <%= link_to dbviewer.table_path(table[:name], table_url_params),
11
+ title: table[:name],
12
+ class: "list-group-item list-group-item-action d-flex align-items-center #{'active' if current_table?(table[:name])}",
13
+ tabindex: "0",
14
+ data: { table_name: table[:name] },
15
+ onkeydown: "
16
+ if(event.key === 'ArrowDown') {
17
+ event.preventDefault();
18
+ let next = this.nextElementSibling;
19
+ while(next && next.classList.contains('d-none')) {
20
+ next = next.nextElementSibling;
21
+ }
22
+ if(next) next.focus();
23
+ } else if(event.key === 'ArrowUp') {
24
+ event.preventDefault();
25
+ let prev = this.previousElementSibling;
26
+ while(prev && prev.classList.contains('d-none')) {
27
+ prev = prev.previousElementSibling;
28
+ }
29
+ if(prev) prev.focus();
30
+ }" do %>
31
+ <div class="d-flex justify-content-between align-items-center w-100">
32
+ <div class="text-truncate">
33
+ <i class="bi bi-table me-2 small"></i>
34
+ <span><%= table[:name] %></span>
35
+ </div>
36
+ <% if table[:record_count].present? %>
37
+ <span class="badge bg-light text-dark fw-normal">
38
+ <%= number_with_delimiter(table[:record_count]) %>
39
+ </span>
40
+ <% end %>
41
+ </div>
42
+ <% end %>
43
+ <% end %>
44
+ <% else %>
45
+ <div class="list-group-item text-muted small">
46
+ <i class="bi bi-info-circle me-1"></i>
47
+ No tables found in this database
48
+ </div>
49
+ <% end %>
@@ -4,14 +4,6 @@
4
4
 
5
5
  <div class="d-flex justify-content-between align-items-center mb-4">
6
6
  <h1>Database Tables</h1>
7
- <div>
8
- <%= link_to dashboard_path, class: "btn btn-outline-primary me-2" do %>
9
- <i class="bi bi-house-door me-1"></i> Dashboard
10
- <% end %>
11
- <%= link_to entity_relationship_diagrams_path, class: "btn btn-outline-primary" do %>
12
- <i class="bi bi-diagram-3 me-1"></i> View ERD
13
- <% end %>
14
- </div>
15
7
  </div>
16
8
 
17
9
  <% if flash[:error] %>
@@ -120,6 +120,15 @@
120
120
  background-color: var(--bs-tertiary-bg, #f8f9fa);
121
121
  }
122
122
 
123
+ /* Styling for disabled input fields (IS NULL, IS NOT NULL) */
124
+ .column-filter:disabled, .disabled-filter {
125
+ background-color: var(--bs-tertiary-bg, #f0f0f0);
126
+ border-color: var(--bs-border-color, #dee2e6);
127
+ color: var(--bs-secondary-color, #6c757d);
128
+ opacity: 0.6;
129
+ cursor: not-allowed;
130
+ }
131
+
123
132
  /* Action column styling */
124
133
  .action-column {
125
134
  width: 60px;
@@ -777,7 +786,11 @@
777
786
  const primaryKeyValue = recordData[Object.keys(recordData).find(key => key === 'id') || Object.keys(recordData)[0]];
778
787
 
779
788
  if (primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== '') {
780
- relationshipsContent.appendChild(createRelationshipSection('Has Many', reverseForeignKeys, recordData, 'has_many', primaryKeyValue));
789
+ const hasManySection = createRelationshipSection('Has Many', reverseForeignKeys, recordData, 'has_many', primaryKeyValue);
790
+ relationshipsContent.appendChild(hasManySection);
791
+
792
+ // Fetch relationship counts asynchronously
793
+ fetchRelationshipCounts('<%= @table_name %>', primaryKeyValue, reverseForeignKeys, hasManySection);
781
794
  }
782
795
  }
783
796
 
@@ -814,11 +827,61 @@
814
827
  };
815
828
  }
816
829
 
830
+ // Function to handle operator changes for IS NULL and IS NOT NULL operators
831
+ function setupNullOperators() {
832
+ operatorSelects.forEach(select => {
833
+ // Initial setup for existing null operators
834
+ if (select.value === 'is_null' || select.value === 'is_not_null') {
835
+ const columnName = select.name.match(/\[(.*?)_operator\]/)[1];
836
+ const inputContainer = select.closest('.filter-input-group');
837
+ // Check for display field (the visible disabled field)
838
+ const displayField = inputContainer.querySelector(`[data-column="${columnName}_display"]`);
839
+ if (displayField) {
840
+ displayField.classList.add('disabled-filter');
841
+ }
842
+
843
+ // Make sure the value field properly reflects the null operator
844
+ const valueField = inputContainer.querySelector(`[data-column="${columnName}"]`);
845
+ if (valueField) {
846
+ valueField.value = select.value;
847
+ }
848
+ }
849
+
850
+ // Handle operator changes
851
+ select.addEventListener('change', function() {
852
+ const columnName = this.name.match(/\[(.*?)_operator\]/)[1];
853
+ const filterForm = this.closest('form');
854
+ const inputContainer = this.closest('.filter-input-group');
855
+ const hiddenField = inputContainer.querySelector(`[data-column="${columnName}"]`);
856
+ const displayField = inputContainer.querySelector(`[data-column="${columnName}_display"]`);
857
+ const wasNullOperator = hiddenField && (hiddenField.value === 'is_null' || hiddenField.value === 'is_not_null');
858
+ const isNullOperator = this.value === 'is_null' || this.value === 'is_not_null';
859
+
860
+ if (isNullOperator) {
861
+ // Configure for null operator
862
+ if (hiddenField) {
863
+ hiddenField.value = this.value;
864
+ }
865
+ // Submit immediately
866
+ filterForm.submit();
867
+ } else if (wasNullOperator) {
868
+ // Clear value when switching from null operator to regular operator
869
+ if (hiddenField) {
870
+ hiddenField.value = '';
871
+ }
872
+ }
873
+ });
874
+ });
875
+ }
876
+
817
877
  // Function to submit the form
818
878
  const submitForm = debounce(function() {
819
879
  filterForm.submit();
820
880
  }, 500);
821
881
 
882
+ // Initialize the null operators handling
883
+ setupNullOperators();
884
+
822
885
  // Add event listeners to all filter inputs
823
886
  columnFilters.forEach(function(filter) {
824
887
  // For text fields use input event
@@ -1433,6 +1496,55 @@
1433
1496
  });
1434
1497
 
1435
1498
  // Helper function to create relationship sections
1499
+ // Function to fetch relationship counts from API
1500
+ async function fetchRelationshipCounts(tableName, recordId, relationships, hasManySection) {
1501
+ try {
1502
+ const response = await fetch(`/dbviewer/api/tables/${tableName}/relationship_counts?record_id=${recordId}`);
1503
+ if (!response.ok) {
1504
+ throw new Error(`HTTP error! status: ${response.status}`);
1505
+ }
1506
+
1507
+ const data = await response.json();
1508
+
1509
+ // Update each count in the UI
1510
+ const countSpans = hasManySection.querySelectorAll('.relationship-count');
1511
+
1512
+ relationships.forEach((relationship, index) => {
1513
+ const countSpan = countSpans[index];
1514
+ if (countSpan) {
1515
+ const relationshipData = data.relationships.find(r =>
1516
+ r.table === relationship.from_table && r.foreign_key === relationship.column
1517
+ );
1518
+
1519
+ if (relationshipData) {
1520
+ const count = relationshipData.count;
1521
+ let badgeClass = 'bg-secondary';
1522
+ let badgeText = `${count} record${count !== 1 ? 's' : ''}`;
1523
+
1524
+ // Use different colors based on count
1525
+ if (count > 0) {
1526
+ badgeClass = count > 10 ? 'bg-warning' : 'bg-success';
1527
+ }
1528
+
1529
+ countSpan.innerHTML = `<span class="badge ${badgeClass}">${badgeText}</span>`;
1530
+ } else {
1531
+ // Fallback if no data found
1532
+ countSpan.innerHTML = '<span class="badge bg-danger">Error</span>';
1533
+ }
1534
+ }
1535
+ });
1536
+
1537
+ } catch (error) {
1538
+ console.error('Error fetching relationship counts:', error);
1539
+
1540
+ // Show error state in UI
1541
+ const countSpans = hasManySection.querySelectorAll('.relationship-count');
1542
+ countSpans.forEach(span => {
1543
+ span.innerHTML = '<span class="badge bg-danger">Error</span>';
1544
+ });
1545
+ }
1546
+ }
1547
+
1436
1548
  function createRelationshipSection(title, relationships, recordData, type, primaryKeyValue = null) {
1437
1549
  const section = document.createElement('div');
1438
1550
  section.className = 'relationship-section mb-4';
@@ -1503,7 +1615,12 @@
1503
1615
  <span class="text-muted">${fk.from_table}.</span><strong>${fk.column}</strong>
1504
1616
  </td>
1505
1617
  <td>
1506
- <span class="badge bg-secondary">View All</span>
1618
+ <span class="relationship-count">
1619
+ <span class="badge bg-secondary">
1620
+ <span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
1621
+ Loading...
1622
+ </span>
1623
+ </span>
1507
1624
  </td>
1508
1625
  <td>
1509
1626
  <a href="/dbviewer/tables/${fk.from_table}?column_filters[${fk.column}]=${encodeURIComponent(primaryKeyValue)}"
@@ -2108,12 +2225,6 @@
2108
2225
  background: transparent !important;
2109
2226
  }
2110
2227
 
2111
- [data-bs-theme="dark"] .flatpickr-day.disabled:hover {
2112
- background: transparent !important;
2113
- color: #6c757d !important;
2114
- cursor: not-allowed;
2115
- }
2116
-
2117
2228
  /* Dark mode other day states */
2118
2229
  [data-bs-theme="dark"] .flatpickr-day.nextMonthDay,
2119
2230
  [data-bs-theme="dark"] .flatpickr-day.prevMonthDay {
@@ -2263,6 +2374,19 @@
2263
2374
  .flatpickr-next-month:hover {
2264
2375
  background: rgba(var(--bs-primary-rgb), 0.1);
2265
2376
  }
2377
+
2378
+ /* Relationship count styling */
2379
+ .relationship-count .badge {
2380
+ min-width: 80px;
2381
+ display: inline-flex;
2382
+ align-items: center;
2383
+ justify-content: center;
2384
+ }
2385
+
2386
+ .relationship-count .spinner-border-sm {
2387
+ width: 0.875rem;
2388
+ height: 0.875rem;
2389
+ }
2266
2390
  </style>
2267
2391
 
2268
2392
  <script>
@@ -1357,6 +1357,27 @@
1357
1357
  <% end %>
1358
1358
  </ul>
1359
1359
  <ul class="navbar-nav ms-auto">
1360
+ <li class="nav-item dropdown">
1361
+ <a class="nav-link dropdown-toggle" href="#" id="navbarDatabaseDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
1362
+ <i class="bi bi-database"></i> <%= (current_conn = available_connections.find { |c| c[:current] }) ? current_conn[:name] : "Database" %>
1363
+ </a>
1364
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDatabaseDropdown">
1365
+ <% available_connections.each do |connection| %>
1366
+ <li>
1367
+ <%= button_to connection_path(connection[:key]), method: :post, class: "dropdown-item border-0 w-100 text-start #{'active' if connection[:current]}" do %>
1368
+ <% if connection[:current] %>
1369
+ <i class="bi bi-check2-circle me-2"></i>
1370
+ <% else %>
1371
+ <i class="bi bi-circle me-2"></i>
1372
+ <% end %>
1373
+ <%= connection[:name] %>
1374
+ <% end %>
1375
+ </li>
1376
+ <% end %>
1377
+ <li><hr class="dropdown-divider"></li>
1378
+ <li><%= link_to "<i class='bi bi-gear'></i> Manage Connections".html_safe, connections_path, class: "dropdown-item" %></li>
1379
+ </ul>
1380
+ </li>
1360
1381
  <li class="nav-item">
1361
1382
  <button type="button" class="theme-toggle nav-link" aria-label="<%= theme_toggle_label %>">
1362
1383
  <%= theme_toggle_icon %>
@@ -1364,7 +1385,7 @@
1364
1385
  </li>
1365
1386
  <li class="nav-item">
1366
1387
  <span class="navbar-text ms-2 text-light d-flex align-items-center">
1367
- <small><i class="bi bi-database"></i> <%= Rails.env %> environment</small>
1388
+ <small><i class="bi bi-tools"></i> <%= Rails.env %> environment</small>
1368
1389
  </span>
1369
1390
  </li>
1370
1391
  </ul>
data/config/routes.rb CHANGED
@@ -10,6 +10,12 @@ Dbviewer::Engine.routes.draw do
10
10
 
11
11
  resources :entity_relationship_diagrams, only: [ :index ]
12
12
 
13
+ resources :connections, only: [ :index, :new, :create, :destroy ] do
14
+ member do
15
+ post :update
16
+ end
17
+ end
18
+
13
19
  resources :logs, only: [ :index ] do
14
20
  collection do
15
21
  delete :destroy_all
@@ -25,6 +31,9 @@ Dbviewer::Engine.routes.draw do
25
31
  get "records"
26
32
  get "relationships_count"
27
33
  end
34
+ member do
35
+ get "relationship_counts"
36
+ end
28
37
  end
29
38
 
30
39
  resources :entity_relationship_diagrams, only: [] do
@@ -43,6 +52,12 @@ Dbviewer::Engine.routes.draw do
43
52
  get "recent"
44
53
  end
45
54
  end
55
+
56
+ resources :connections, only: [] do
57
+ member do
58
+ get "test"
59
+ end
60
+ end
46
61
  end
47
62
 
48
63
  root to: "home#index"
@@ -41,6 +41,16 @@ module Dbviewer
41
41
  # Default column to order table details by (e.g., 'updated_at')
42
42
  attr_accessor :default_order_column
43
43
 
44
+ # Multiple database connections configuration
45
+ # @example {
46
+ # primary: { connection_class: "ActiveRecord::Base", name: "Primary DB" },
47
+ # secondary: { connection_class: "SomeClass", name: "Secondary DB" }
48
+ # }
49
+ attr_accessor :database_connections
50
+
51
+ # The key of the current active connection
52
+ attr_accessor :current_connection
53
+
44
54
  def initialize
45
55
  @per_page_options = [ 10, 20, 50, 100 ]
46
56
  @default_per_page = 20
@@ -55,6 +65,13 @@ module Dbviewer
55
65
  @enable_query_logging = true
56
66
  @admin_credentials = nil
57
67
  @default_order_column = "updated_at"
68
+ @database_connections = {
69
+ default: {
70
+ connection_class: "ActiveRecord::Base",
71
+ name: "Default Database"
72
+ }
73
+ }
74
+ @current_connection = :default
58
75
  end
59
76
  end
60
77
  end
@@ -14,8 +14,7 @@ module Dbviewer
14
14
  # @param table_name [String] Name of the table
15
15
  # @return [Class] ActiveRecord model class for the table
16
16
  def get_model_for(table_name)
17
- cached_model = @cache_manager.get_model(table_name)
18
- return cached_model if cached_model
17
+ return @cache_manager.get_model(table_name) if @cache_manager.has_model?(table_name)
19
18
 
20
19
  model = create_model_for(table_name)
21
20
  @cache_manager.store_model(table_name, model)
@@ -28,10 +27,7 @@ module Dbviewer
28
27
  # @param table_name [String] Name of the table
29
28
  # @return [Class] ActiveRecord model class for the table
30
29
  def create_model_for(table_name)
31
- model_name = table_name.classify
32
-
33
- # Create a new model class dynamically
34
- model = Class.new(ActiveRecord::Base) do
30
+ model = Dbviewer.const_set(table_name.classify, Class.new(ActiveRecord::Base) do
35
31
  self.table_name = table_name
36
32
 
37
33
  # Some tables might not have primary keys, so we handle that
@@ -47,13 +43,9 @@ module Dbviewer
47
43
 
48
44
  # Disable timestamps for better compatibility
49
45
  self.record_timestamps = false
50
- end
46
+ end)
51
47
 
52
- # Set model name constant if not already taken
53
- # Use a namespace to avoid polluting the global namespace
54
- unless Dbviewer.const_defined?("DynamicModel_#{model_name}")
55
- Dbviewer.const_set("DynamicModel_#{model_name}", model)
56
- end
48
+ model.establish_connection(@connection.instance_variable_get(:@config))
57
49
 
58
50
  model
59
51
  end
@@ -3,10 +3,12 @@ module Dbviewer
3
3
  # Manager handles all database interactions for the DBViewer engine
4
4
  # It provides methods to access database structure and data
5
5
  class Manager
6
- attr_reader :connection, :adapter_name, :table_query_operations
6
+ attr_reader :connection, :adapter_name, :table_query_operations, :connection_key
7
7
 
8
8
  # Initialize the database manager
9
- def initialize
9
+ # @param connection_key [Symbol] The key identifying the connection in configuration
10
+ def initialize(connection_key = nil)
11
+ @connection_key = connection_key || Dbviewer.configuration.current_connection
10
12
  ensure_connection
11
13
  @cache_manager = ::Dbviewer::Database::CacheManager.new(configuration.cache_expiry)
12
14
  @table_metadata_manager = ::Dbviewer::Database::MetadataManager.new(@connection, @cache_manager)
@@ -151,6 +153,13 @@ module Dbviewer
151
153
  end
152
154
  end
153
155
 
156
+ # Get a dynamic AR model for a table
157
+ # @param table_name [String] Name of the table
158
+ # @return [Class] ActiveRecord model class
159
+ def get_model_for(table_name)
160
+ @dynamic_model_factory.get_model_for(table_name)
161
+ end
162
+
154
163
  private
155
164
 
156
165
  def fetch_mysql_size
@@ -182,8 +191,15 @@ module Dbviewer
182
191
  # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter] The database connection
183
192
  def ensure_connection
184
193
  return @connection if @connection
194
+ connection_config = Dbviewer.configuration.database_connections[@connection_key]
195
+
196
+ if connection_config && connection_config[:connection_class]
197
+ @connection = connection_config[:connection_class].constantize.connection
198
+ else
199
+ Rails.logger.warn "DBViewer: Using default connection for key: #{@connection_key}"
200
+ @connection = ActiveRecord::Base.connection
201
+ end
185
202
 
186
- @connection = ActiveRecord::Base.connection
187
203
  @adapter_name = @connection.adapter_name.downcase
188
204
  @connection
189
205
  end
@@ -192,13 +208,6 @@ module Dbviewer
192
208
  def reset_cache_if_needed
193
209
  @cache_manager.reset_if_needed
194
210
  end
195
-
196
- # Get a dynamic AR model for a table
197
- # @param table_name [String] Name of the table
198
- # @return [Class] ActiveRecord model class
199
- def get_model_for(table_name)
200
- @dynamic_model_factory.get_model_for(table_name)
201
- end
202
211
  end
203
212
  end
204
213
  end