dbviewer 0.5.8 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d46526c59b9c355c9e717e76ebe81299bbdb051be58fb1b789fbe2fa1c50c0e4
4
- data.tar.gz: 713277fda9b00b0fe4a48b2e8324632f2c7920b25cbaad369afe2d159c2bcf29
3
+ metadata.gz: 53152d4109f427b7d3d32f38292f6c7add1aecc15e7bf9ce8db74458f24f8de8
4
+ data.tar.gz: dc556f4746314538c53a16cd1eabd811b91cfb6c35bee177971de3eae40f9f93
5
5
  SHA512:
6
- metadata.gz: 77444f6cf1b7d847d114bdef9f965d9c90e4e216666db1fccb7de5652f92950cae7793bab07da982e473517f4c350d0dc186e094291138258c9a589de983bf5c
7
- data.tar.gz: fc6801c693d632bf8a4bfe26d0aa4b0614ba8570867cf14575f279813c2b8599e5f3288f1bc0175d2db108d62ec2362d484c47a2e0059d5eb28c1a4d4a091d52
6
+ metadata.gz: e4b267eee75e4a92a6a139e226834ff051e306524e1450833b644467b3661c83bffcadb9920450f1196c12919b474f6dca7c3cc6b12f352899c5803984c930e7
7
+ data.tar.gz: f62a2d830f539f8636522035fa60e08535f27c8b0cb77eed42c4baf1103171533e1b8327975a696f5d3c7125bfbc9ae4dc20c1a21d145b3f7b56117f95ccb742
data/README.md CHANGED
@@ -32,6 +32,11 @@ It's designed for development, debugging, and database analysis, offering a clea
32
32
  - View table structure reference while writing queries
33
33
  - Protection against potentially harmful SQL operations
34
34
  - Query execution statistics and timing
35
+ - **Multiple Database Connections**:
36
+ - Connect to multiple databases within your application
37
+ - Switch between connections on-the-fly to view different database schemas
38
+ - Add new database connections from the UI without code changes
39
+ - Test connections to verify they're working properly
35
40
  - **Enhanced UI Features**:
36
41
  - Responsive, Bootstrap-based interface that works on desktop and mobile
37
42
  - Fixed header navigation with quick access to all features
@@ -142,6 +147,34 @@ You can also create this file manually if you prefer.
142
147
 
143
148
  The configuration is accessed through `Dbviewer.configuration` throughout the codebase. You can also access it via `Dbviewer.config` which is an alias for backward compatibility.
144
149
 
150
+ ### Multiple Database Connections
151
+
152
+ DBViewer supports working with multiple database connections in your application. This is useful for applications that connect to multiple databases or use different connection pools.
153
+
154
+ To configure multiple database connections, set them up in your initializer:
155
+
156
+ ```ruby
157
+ # config/initializers/dbviewer.rb
158
+ Dbviewer.configure do |config|
159
+ # Multiple database connections configuration
160
+ config.database_connections = {
161
+ primary: {
162
+ connection_class: "ActiveRecord::Base",
163
+ name: "Primary Database"
164
+ },
165
+ secondary: {
166
+ connection_class: "SecondaryDatabase",
167
+ name: "Blog Database"
168
+ }
169
+ }
170
+
171
+ # Set the default active connection
172
+ config.current_connection = :primary
173
+ end
174
+ ```
175
+
176
+ Each connection needs to reference an ActiveRecord class that establishes a database connection. For more details, see [Multiple Database Connections](docs/multiple_connections.md).
177
+
145
178
  ## 🪵 Query Logging
146
179
 
147
180
  DBViewer includes a powerful SQL query logging system that captures and analyzes database queries. You can access this log through the `/dbviewer/logs` endpoint. The logging system offers two storage backends:
@@ -413,12 +446,6 @@ graph TB
413
446
  Engine -.->|"setup()"| QueryLogger
414
447
  Config -.->|"logging settings"| QueryLogger
415
448
 
416
- classDef decoupled fill:#e1f5fe,stroke:#01579b,stroke-width:2px
417
- classDef controller fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
418
- classDef database fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
419
- classDef query fill:#fff3e0,stroke:#e65100,stroke-width:2px
420
- classDef storage fill:#fce4ec,stroke:#880e4f,stroke-width:2px
421
-
422
449
  class CacheManager,QueryLogger decoupled
423
450
  class HomeController,TablesController,LogsController,ERDController,APIController controller
424
451
  class Manager,MetadataManager,DynamicModelFactory database
@@ -0,0 +1,70 @@
1
+
2
+ document.addEventListener('DOMContentLoaded', function() {
3
+ // Add connection form validation
4
+ const forms = document.querySelectorAll('.needs-validation');
5
+
6
+ Array.from(forms).forEach(form => {
7
+ form.addEventListener('submit', event => {
8
+ if (!form.checkValidity()) {
9
+ event.preventDefault();
10
+ event.stopPropagation();
11
+ }
12
+
13
+ form.classList.add('was-validated');
14
+ }, false);
15
+ });
16
+
17
+ // Handle connection test
18
+ const testButtons = document.querySelectorAll('.test-connection-btn');
19
+
20
+ Array.from(testButtons).forEach(button => {
21
+ button.addEventListener('click', async function(event) {
22
+ event.preventDefault();
23
+ const connectionKey = this.dataset.connectionKey;
24
+ const statusElement = document.getElementById(`connection-status-${connectionKey}`);
25
+
26
+ if (!statusElement) return;
27
+
28
+ statusElement.innerHTML = '<i class="bi bi-arrow-repeat spin me-1"></i> Testing connection...';
29
+ statusElement.classList.remove('text-success', 'text-danger');
30
+
31
+ try {
32
+ const response = await fetch(`/dbviewer/api/connections/${connectionKey}/test`, {
33
+ method: 'GET',
34
+ headers: {
35
+ 'Accept': 'application/json',
36
+ 'X-Requested-With': 'XMLHttpRequest'
37
+ }
38
+ });
39
+
40
+ const data = await response.json();
41
+
42
+ if (data.success) {
43
+ statusElement.innerHTML = '<i class="bi bi-check-circle-fill me-1"></i> Connection successful';
44
+ statusElement.classList.add('text-success');
45
+ } else {
46
+ statusElement.innerHTML = `<i class="bi bi-x-circle-fill me-1"></i> ${data.error || 'Connection failed'}`;
47
+ statusElement.classList.add('text-danger');
48
+ }
49
+ } catch (error) {
50
+ statusElement.innerHTML = '<i class="bi bi-x-circle-fill me-1"></i> Error testing connection';
51
+ statusElement.classList.add('text-danger');
52
+ }
53
+ });
54
+ });
55
+ });
56
+
57
+ // Auto-generate connection key from name
58
+ function setupConnectionKeyGenerator() {
59
+ const nameInput = document.getElementById('connection_name');
60
+ const keyInput = document.getElementById('connection_key');
61
+
62
+ if (nameInput && keyInput) {
63
+ nameInput.addEventListener('input', function() {
64
+ keyInput.value = this.value
65
+ .toLowerCase()
66
+ .replace(/\s+/g, '_')
67
+ .replace(/[^a-z0-9_]/g, '');
68
+ });
69
+ }
70
+ }
@@ -5,12 +5,100 @@ module Dbviewer
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- helper_method :current_table?, :get_database_name, :get_adapter_name if respond_to?(:helper_method)
8
+ helper_method :current_table?, :get_database_name, :get_adapter_name,
9
+ :current_connection_key, :available_connections if respond_to?(:helper_method)
9
10
  end
10
11
 
11
- # Initialize the database manager
12
+ # Get the current active connection key
13
+ def current_connection_key
14
+ # Get the connection key from the session or fall back to the default
15
+ key = session[:dbviewer_connection] || Dbviewer.configuration.current_connection
16
+
17
+ # Ensure the key actually exists in our configured connections
18
+ if key && Dbviewer.configuration.database_connections.key?(key.to_sym)
19
+ return key.to_sym
20
+ end
21
+
22
+ # If the key doesn't exist, fall back to any available connection
23
+ first_key = Dbviewer.configuration.database_connections.keys.first
24
+ if first_key
25
+ session[:dbviewer_connection] = first_key # Update the session
26
+ return first_key
27
+ end
28
+
29
+ # If there are no connections configured, use a default key
30
+ # This should never happen in normal operation, but it's a safety measure
31
+ :default
32
+ end
33
+
34
+ # Set the current connection to use
35
+ def switch_connection(connection_key)
36
+ connection_key = connection_key.to_sym if connection_key.respond_to?(:to_sym)
37
+
38
+ if connection_key && Dbviewer.configuration.database_connections.key?(connection_key)
39
+ session[:dbviewer_connection] = connection_key
40
+ # Clear the database manager to force it to be recreated with the new connection
41
+ @database_manager = nil
42
+ return true
43
+ else
44
+ # If the connection key doesn't exist, reset to default connection
45
+ if Dbviewer.configuration.database_connections.key?(Dbviewer.configuration.current_connection)
46
+ session[:dbviewer_connection] = Dbviewer.configuration.current_connection
47
+ @database_manager = nil
48
+ return true
49
+ else
50
+ # If even the default connection isn't valid, try the first available connection
51
+ first_key = Dbviewer.configuration.database_connections.keys.first
52
+ if first_key
53
+ session[:dbviewer_connection] = first_key
54
+ @database_manager = nil
55
+ return true
56
+ end
57
+ end
58
+ end
59
+
60
+ false # Return false if we couldn't set a valid connection
61
+ end
62
+
63
+ # Get list of available connections
64
+ def available_connections
65
+ connections = Dbviewer.configuration.database_connections.map do |key, config|
66
+ # Try to determine the adapter name if it's not already stored
67
+ adapter_name = nil
68
+ if config[:adapter_name].present?
69
+ adapter_name = config[:adapter_name]
70
+ elsif config[:connection].present?
71
+ begin
72
+ adapter_name = config[:connection].connection.adapter_name
73
+ rescue => e
74
+ Rails.logger.error("Error getting adapter name: #{e.message}")
75
+ end
76
+ end
77
+
78
+ {
79
+ key: key,
80
+ name: config[:name] || key.to_s.humanize,
81
+ adapter_name: adapter_name,
82
+ current: key.to_sym == current_connection_key.to_sym
83
+ }
84
+ end
85
+
86
+ # Ensure at least one connection is marked as current
87
+ unless connections.any? { |c| c[:current] }
88
+ # If no connection is current, mark the first one as current
89
+ if connections.any?
90
+ connections.first[:current] = true
91
+ # Also update the session
92
+ session[:dbviewer_connection] = connections.first[:key]
93
+ end
94
+ end
95
+
96
+ connections
97
+ end
98
+
99
+ # Initialize the database manager with the current connection
12
100
  def database_manager
13
- @database_manager ||= ::Dbviewer::Database::Manager.new
101
+ @database_manager = ::Dbviewer::Database::Manager.new(current_connection_key)
14
102
  end
15
103
 
16
104
  # Initialize the table query operations manager
@@ -21,6 +109,12 @@ module Dbviewer
21
109
 
22
110
  # Get the name of the current database
23
111
  def get_database_name
112
+ # First check if this connection has a name in the configuration
113
+ current_conn_config = Dbviewer.configuration.database_connections[current_connection_key]
114
+ if current_conn_config && current_conn_config[:name].present?
115
+ return current_conn_config[:name]
116
+ end
117
+
24
118
  adapter = database_manager.connection.adapter_name.downcase
25
119
 
26
120
  case adapter
@@ -34,7 +128,7 @@ module Dbviewer
34
128
  result ? result["db_name"] : "Database"
35
129
  when /sqlite/
36
130
  # For SQLite, extract the database name from the connection_config
37
- database_path = ActiveRecord::Base.connection.pool.spec.config[:database] || ""
131
+ database_path = database_manager.connection.pool.spec.config[:database] || ""
38
132
  File.basename(database_path, ".*") || "SQLite Database"
39
133
  else
40
134
  "Database" # Default fallback
@@ -68,11 +162,21 @@ module Dbviewer
68
162
  name: table_name
69
163
  }
70
164
 
71
- # Only fetch record counts if explicitly requested
72
- table_stats[:record_count] = database_manager.record_count(table_name) if include_record_counts
165
+ # Only fetch record count if specifically requested
166
+ if include_record_counts
167
+ begin
168
+ table_stats[:record_count] = database_manager.record_count(table_name)
169
+ rescue => e
170
+ Rails.logger.error("Error fetching record count for #{table_name}: #{e.message}")
171
+ table_stats[:record_count] = 0
172
+ end
173
+ end
73
174
 
74
175
  table_stats
75
176
  end
177
+ rescue => e
178
+ Rails.logger.error("Error fetching tables: #{e.message}")
179
+ []
76
180
  end
77
181
 
78
182
  # Gather database analytics information
@@ -4,74 +4,44 @@ module Dbviewer
4
4
  before_action :set_tables
5
5
 
6
6
  def relationships
7
- # Fetch all relationships asynchronously
8
- begin
9
- @table_relationships = fetch_table_relationships
10
- render_success({
11
- relationships: @table_relationships,
12
- status: "success"
13
- })
14
- rescue => e
15
- Rails.logger.error("[DBViewer] Error fetching relationships: #{e.message}")
16
- render json: {
17
- relationships: [],
18
- status: "error",
19
- error: e.message
20
- }, status: :internal_server_error
21
- end
7
+ @table_relationships = fetch_table_relationships
8
+ render_success({
9
+ relationships: @table_relationships,
10
+ status: "success"
11
+ })
22
12
  end
23
13
 
24
14
  def table_relationships
25
- # Fetch relationships for specific tables
26
15
  table_names = params[:tables]&.split(",") || []
27
-
28
- if table_names.blank?
29
- render json: {
30
- relationships: [],
31
- status: "error",
32
- error: "No tables specified"
33
- }, status: :bad_request
34
- return
35
- end
36
-
37
- begin
38
- relationships = []
39
-
40
- table_names.each do |table_name|
41
- next unless @tables.any? { |t| t[:name] == table_name }
42
-
43
- begin
44
- metadata = fetch_table_metadata(table_name)
45
- if metadata && metadata[:foreign_keys].present?
46
- metadata[:foreign_keys].each do |fk|
47
- relationships << {
48
- from_table: table_name,
49
- to_table: fk[:to_table],
50
- from_column: fk[:column],
51
- to_column: fk[:primary_key],
52
- name: fk[:name]
53
- }
54
- end
16
+ relationships = []
17
+
18
+ table_names.each do |table_name|
19
+ next unless @tables.any? { |t| t[:name] == table_name }
20
+
21
+ begin
22
+ metadata = fetch_table_metadata(table_name)
23
+ if metadata && metadata[:foreign_keys].present?
24
+ metadata[:foreign_keys].each do |fk|
25
+ relationships << {
26
+ from_table: table_name,
27
+ to_table: fk[:to_table],
28
+ from_column: fk[:column],
29
+ to_column: fk[:primary_key],
30
+ name: fk[:name]
31
+ }
55
32
  end
56
- rescue => e
57
- Rails.logger.error("[DBViewer] Error fetching relationships for #{table_name}: #{e.message}")
58
- # Continue with other tables even if one fails
59
33
  end
34
+ rescue => e
35
+ Rails.logger.error("[DBViewer] Error fetching relationships for #{table_name}: #{e.message}")
36
+ # Continue with other tables even if one fails
60
37
  end
61
-
62
- render_success({
63
- relationships: relationships,
64
- status: "success",
65
- processed_tables: table_names
66
- })
67
- rescue => e
68
- Rails.logger.error("[DBViewer] Error in table_relationships: #{e.message}")
69
- render json: {
70
- relationships: [],
71
- status: "error",
72
- error: e.message
73
- }, status: :internal_server_error
74
38
  end
39
+
40
+ render_success({
41
+ relationships: relationships,
42
+ status: "success",
43
+ processed_tables: table_names
44
+ })
75
45
  end
76
46
 
77
47
  private
@@ -20,55 +20,20 @@ module Dbviewer
20
20
  table_name = params[:id]
21
21
  record_id = params[:record_id]
22
22
 
23
- unless table_name.present? && record_id.present?
24
- render_error("Table name and record ID are required", 400)
25
- return
23
+ reverse_foreign_keys = fetch_table_metadata(table_name).dig(:reverse_foreign_keys) || []
24
+ relationship_counts = reverse_foreign_keys.map do |rel|
25
+ {
26
+ table: rel[:from_table],
27
+ foreign_key: rel[:column],
28
+ count: database_manager.get_model_for(rel[:from_table]).where(rel[:column] => record_id).count
29
+ }
26
30
  end
27
31
 
28
- begin
29
- # Get table metadata to find relationships
30
- metadata = fetch_table_metadata(table_name)
31
-
32
- unless metadata
33
- render_error("Table not found", 404)
34
- return
35
- end
36
-
37
- # Get reverse foreign keys (has_many relationships)
38
- reverse_foreign_keys = metadata.dig(:reverse_foreign_keys) || []
39
-
40
- relationship_counts = reverse_foreign_keys.map do |rel|
41
- begin
42
- # Count records in the related table that reference this record
43
- count_query = "SELECT COUNT(*) as count FROM #{rel[:from_table]} WHERE #{rel[:column]} = ?"
44
- result = database_manager.connection.exec_query(count_query, "Count Query", [ record_id ])
45
- count = result.rows.first&.first || 0
46
-
47
- {
48
- table: rel[:from_table],
49
- foreign_key: rel[:column],
50
- count: count.to_i
51
- }
52
- rescue => e
53
- Rails.logger.error "Error counting relationships for #{rel[:from_table]}: #{e.message}"
54
- {
55
- table: rel[:from_table],
56
- foreign_key: rel[:column],
57
- count: 0,
58
- error: e.message
59
- }
60
- end
61
- end
62
-
63
- render_success({
64
- table_name: table_name,
65
- record_id: record_id,
66
- relationships: relationship_counts
67
- })
68
- rescue => e
69
- Rails.logger.error "Error fetching relationship counts: #{e.message}"
70
- render_error("Error fetching relationship counts: #{e.message}", 500)
71
- end
32
+ render_success({
33
+ table_name: table_name,
34
+ record_id: record_id,
35
+ relationships: relationship_counts
36
+ })
72
37
  end
73
38
 
74
39
  private
@@ -0,0 +1,35 @@
1
+ module Dbviewer
2
+ class ConnectionsController < ApplicationController
3
+ # GET /dbviewer/connections
4
+ def index
5
+ @connections = available_connections
6
+
7
+ respond_to do |format|
8
+ format.html
9
+ format.json { render json: @connections }
10
+ end
11
+ end
12
+
13
+ # POST /dbviewer/connections/:id
14
+ def update
15
+ connection_key = params[:id]
16
+
17
+ if switch_connection(connection_key)
18
+ # Safely get the connection name
19
+ connection_config = Dbviewer.configuration.database_connections[connection_key.to_sym]
20
+ connection_name = connection_config && connection_config[:name] ? connection_config[:name] : "selected"
21
+
22
+ respond_to do |format|
23
+ format.html { redirect_to root_path, notice: "Switched to #{connection_name} database." }
24
+ format.json { render json: { success: true, current: connection_key } }
25
+ end
26
+ else
27
+ # Handle the case where switching failed
28
+ respond_to do |format|
29
+ format.html { redirect_to connections_path, alert: "Failed to switch connection. The connection may be invalid." }
30
+ format.json { render json: { success: false, error: "Invalid connection" }, status: :unprocessable_entity }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,6 +1,11 @@
1
1
  module Dbviewer
2
2
  class HomeController < ApplicationController
3
3
  def index
4
+ @current_connection_info = {
5
+ name: get_database_name,
6
+ adapter: get_adapter_name,
7
+ key: current_connection_key
8
+ }
4
9
  end
5
10
 
6
11
  private
@@ -33,7 +33,13 @@ module Dbviewer
33
33
 
34
34
  # Generate operator options based on column type
35
35
  def operator_options_for_column_type(column_type)
36
- if column_type && (column_type =~ /datetime/ || column_type =~ /^date$/ || column_type =~ /^time$/)
36
+ # Common operators for all types
37
+ common_operators = [
38
+ [ "is null", "is_null" ],
39
+ [ "is not null", "is_not_null" ]
40
+ ]
41
+
42
+ type_specific_operators = if column_type && (column_type =~ /datetime/ || column_type =~ /^date$/ || column_type =~ /^time$/)
37
43
  # Date/Time operators
38
44
  [
39
45
  [ "=", "eq" ],
@@ -64,28 +70,58 @@ module Dbviewer
64
70
  [ "ends with", "ends_with" ]
65
71
  ]
66
72
  end
73
+
74
+ # Return type-specific operators first, then common operators
75
+ type_specific_operators + common_operators
67
76
  end
68
77
 
69
78
  # Render column filter input based on column type
70
79
  def render_column_filter_input(form, column_name, column_type, column_filters)
71
- if column_type && column_type =~ /datetime/
80
+ # Get selected operator to check if it's a null operator
81
+ operator = column_filters["#{column_name}_operator"]
82
+ is_null_operator = operator == "is_null" || operator == "is_not_null"
83
+
84
+ # Clean up the value for non-null operators if the value contains a null operator
85
+ # This ensures we don't carry over 'is_null' or 'is_not_null' values when switching operators
86
+ value = column_filters[column_name]
87
+ if !is_null_operator && value.present? && (value == "is_null" || value == "is_not_null")
88
+ value = nil
89
+ end
90
+
91
+ # For null operators, display a non-editable field without placeholder
92
+ if is_null_operator
93
+ # Keep a hidden field for the actual value
94
+ hidden_field = form.hidden_field("column_filters[#{column_name}]",
95
+ value: operator,
96
+ class: "null-filter-value",
97
+ data: { column: column_name })
98
+
99
+ # Add a visible but disabled text field with no placeholder or value
100
+ visible_field = form.text_field("column_filters[#{column_name}_display]",
101
+ disabled: true,
102
+ value: "",
103
+ class: "form-control form-control-sm column-filter rounded-0 disabled-filter",
104
+ data: { column: "#{column_name}_display" })
105
+
106
+ hidden_field + visible_field
107
+ elsif column_type && column_type =~ /datetime/
72
108
  form.datetime_local_field("column_filters[#{column_name}]",
73
- value: column_filters[column_name],
109
+ value: value,
74
110
  class: "form-control form-control-sm column-filter rounded-0",
75
111
  data: { column: column_name })
76
112
  elsif column_type && column_type =~ /^date$/
77
113
  form.date_field("column_filters[#{column_name}]",
78
- value: column_filters[column_name],
114
+ value: value,
79
115
  class: "form-control form-control-sm column-filter rounded-0",
80
116
  data: { column: column_name })
81
117
  elsif column_type && column_type =~ /^time$/
82
118
  form.time_field("column_filters[#{column_name}]",
83
- value: column_filters[column_name],
119
+ value: value,
84
120
  class: "form-control form-control-sm column-filter rounded-0",
85
121
  data: { column: column_name })
86
122
  else
87
123
  form.text_field("column_filters[#{column_name}]",
88
- value: column_filters[column_name],
124
+ value: value,
89
125
  placeholder: "",
90
126
  class: "form-control form-control-sm column-filter rounded-0",
91
127
  data: { column: column_name })
@@ -113,8 +149,10 @@ module Dbviewer
113
149
  column_type = column_type_from_info(column_name, columns)
114
150
 
115
151
  content_tag(:div, class: "filter-input-group") do
116
- render_operator_select(form, column_name, column_type, column_filters) +
117
- render_column_filter_input(form, column_name, column_type, column_filters)
152
+ operator_select = render_operator_select(form, column_name, column_type, column_filters)
153
+ input_field = render_column_filter_input(form, column_name, column_type, column_filters)
154
+
155
+ operator_select + input_field
118
156
  end
119
157
  end
120
158
 
@@ -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;
@@ -818,11 +827,61 @@
818
827
  };
819
828
  }
820
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
+
821
877
  // Function to submit the form
822
878
  const submitForm = debounce(function() {
823
879
  filterForm.submit();
824
880
  }, 500);
825
881
 
882
+ // Initialize the null operators handling
883
+ setupNullOperators();
884
+
826
885
  // Add event listeners to all filter inputs
827
886
  columnFilters.forEach(function(filter) {
828
887
  // For text fields use input event
@@ -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
@@ -46,6 +52,12 @@ Dbviewer::Engine.routes.draw do
46
52
  get "recent"
47
53
  end
48
54
  end
55
+
56
+ resources :connections, only: [] do
57
+ member do
58
+ get "test"
59
+ end
60
+ end
49
61
  end
50
62
 
51
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
@@ -273,9 +273,29 @@ module Dbviewer
273
273
  # Apply remaining simple column filters
274
274
  filters.each do |column, value|
275
275
  next unless column_exists?(table_name, column)
276
- next if value.blank?
276
+
277
+ # Check if this is a column operator field
278
+ if column.end_with?("_operator")
279
+ next # Skip operator fields - they're processed with their column
280
+ end
277
281
 
278
282
  column_sym = column.to_sym
283
+ operator = filters["#{column}_operator"]
284
+
285
+ # Special handling for is_null and is_not_null operators that don't need a value
286
+ if operator == "is_null" || value == "is_null"
287
+ Rails.logger.debug("[DBViewer] Applying null filter: #{column} IS NULL")
288
+ query = query.where("#{column} IS NULL")
289
+ next
290
+ elsif operator == "is_not_null" || value == "is_not_null"
291
+ Rails.logger.debug("[DBViewer] Applying not null filter: #{column} IS NOT NULL")
292
+ query = query.where("#{column} IS NOT NULL")
293
+ next
294
+ end
295
+
296
+ # Skip if no value and we're not using a special operator
297
+ next if value.blank? || value == "is_null" || value == "is_not_null"
298
+
279
299
  Rails.logger.debug("[DBViewer] Applying filter: #{column} = #{value}")
280
300
 
281
301
  # Handle different types of filtering
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.5.8"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/dbviewer.rb CHANGED
@@ -57,10 +57,43 @@ module Dbviewer
57
57
  query_logging_mode: configuration.query_logging_mode
58
58
  )
59
59
 
60
- ActiveRecord::Base.connection
61
- Rails.logger.info "DBViewer successfully connected to database"
62
- rescue => e
63
- Rails.logger.error "DBViewer could not connect to database: #{e.message}"
60
+ # Check all configured connections
61
+ connection_errors = []
62
+ configuration.database_connections.each do |key, config|
63
+ begin
64
+ connection_class = nil
65
+
66
+ if config[:connection]
67
+ connection_class = config[:connection]
68
+ elsif config[:connection_class].is_a?(String)
69
+ # Try to load the class if it's defined as a string
70
+ begin
71
+ connection_class = config[:connection_class].constantize
72
+ rescue NameError => e
73
+ Rails.logger.warn "DBViewer could not load connection class #{config[:connection_class]}: #{e.message}"
74
+ next
75
+ end
76
+ end
77
+
78
+ if connection_class
79
+ connection = connection_class.connection
80
+ adapter_name = connection.adapter_name
81
+ Rails.logger.info "DBViewer successfully connected to #{config[:name] || key.to_s} database (#{adapter_name})"
82
+
83
+ # Store the resolved connection class back in the config
84
+ config[:connection] = connection_class
85
+ else
86
+ raise "No valid connection configuration found for #{key}"
87
+ end
88
+ rescue => e
89
+ connection_errors << { key: key, error: e.message }
90
+ Rails.logger.error "DBViewer could not connect to #{config[:name] || key.to_s} database: #{e.message}"
91
+ end
92
+ end
93
+
94
+ if connection_errors.length == configuration.database_connections.length
95
+ raise "DBViewer could not connect to any configured database"
96
+ end
64
97
  end
65
98
 
66
99
  # Initialize engine with default values or user-provided configuration
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.5.8
4
+ version: 0.6.0
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-05-30 00:00:00.000000000 Z
11
+ date: 2025-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -49,6 +49,7 @@ files:
49
49
  - MIT-LICENSE
50
50
  - README.md
51
51
  - Rakefile
52
+ - app/assets/javascripts/dbviewer/connections.js
52
53
  - app/assets/stylesheets/dbviewer/application.css
53
54
  - app/assets/stylesheets/dbviewer/dbviewer.css
54
55
  - app/assets/stylesheets/dbviewer/enhanced.css
@@ -61,6 +62,7 @@ files:
61
62
  - app/controllers/dbviewer/api/queries_controller.rb
62
63
  - app/controllers/dbviewer/api/tables_controller.rb
63
64
  - app/controllers/dbviewer/application_controller.rb
65
+ - app/controllers/dbviewer/connections_controller.rb
64
66
  - app/controllers/dbviewer/entity_relationship_diagrams_controller.rb
65
67
  - app/controllers/dbviewer/home_controller.rb
66
68
  - app/controllers/dbviewer/logs_controller.rb
@@ -69,9 +71,12 @@ files:
69
71
  - app/jobs/dbviewer/application_job.rb
70
72
  - app/mailers/dbviewer/application_mailer.rb
71
73
  - app/models/dbviewer/application_record.rb
74
+ - app/views/dbviewer/connections/index.html.erb
75
+ - app/views/dbviewer/connections/new.html.erb
72
76
  - app/views/dbviewer/entity_relationship_diagrams/index.html.erb
73
77
  - app/views/dbviewer/home/index.html.erb
74
78
  - app/views/dbviewer/logs/index.html.erb
79
+ - app/views/dbviewer/shared/_tables_sidebar.html.erb
75
80
  - app/views/dbviewer/tables/_table_structure.html.erb
76
81
  - app/views/dbviewer/tables/index.html.erb
77
82
  - app/views/dbviewer/tables/mini_erd.html.erb