dbviewer 0.3.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +250 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/dbviewer/application.css +21 -0
- data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
- data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
- data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
- data/app/controllers/dbviewer/application_controller.rb +21 -0
- data/app/controllers/dbviewer/databases_controller.rb +0 -0
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
- data/app/controllers/dbviewer/home_controller.rb +10 -0
- data/app/controllers/dbviewer/logs_controller.rb +39 -0
- data/app/controllers/dbviewer/tables_controller.rb +73 -0
- data/app/helpers/dbviewer/application_helper.rb +118 -0
- data/app/jobs/dbviewer/application_job.rb +4 -0
- data/app/mailers/dbviewer/application_mailer.rb +6 -0
- data/app/models/dbviewer/application_record.rb +5 -0
- data/app/services/dbviewer/file_storage.rb +0 -0
- data/app/services/dbviewer/in_memory_storage.rb +0 -0
- data/app/services/dbviewer/query_analyzer.rb +0 -0
- data/app/services/dbviewer/query_collection.rb +0 -0
- data/app/services/dbviewer/query_logger.rb +0 -0
- data/app/services/dbviewer/query_parser.rb +82 -0
- data/app/services/dbviewer/query_storage.rb +0 -0
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
- data/app/views/dbviewer/home/index.html.erb +237 -0
- data/app/views/dbviewer/logs/index.html.erb +614 -0
- data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
- data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
- data/app/views/dbviewer/tables/index.html.erb +128 -0
- data/app/views/dbviewer/tables/query.html.erb +600 -0
- data/app/views/dbviewer/tables/show.html.erb +271 -0
- data/app/views/layouts/dbviewer/application.html.erb +728 -0
- data/config/routes.rb +22 -0
- data/lib/dbviewer/configuration.rb +79 -0
- data/lib/dbviewer/database_manager.rb +450 -0
- data/lib/dbviewer/engine.rb +20 -0
- data/lib/dbviewer/initializer.rb +23 -0
- data/lib/dbviewer/logger.rb +102 -0
- data/lib/dbviewer/query_analyzer.rb +109 -0
- data/lib/dbviewer/query_collection.rb +41 -0
- data/lib/dbviewer/query_parser.rb +82 -0
- data/lib/dbviewer/sql_validator.rb +194 -0
- data/lib/dbviewer/storage/base.rb +31 -0
- data/lib/dbviewer/storage/file_storage.rb +96 -0
- data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
- data/lib/dbviewer/version.rb +3 -0
- data/lib/dbviewer.rb +65 -0
- data/lib/tasks/dbviewer_tasks.rake +4 -0
- metadata +126 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
<div class="dbviewer-sidebar-top">
|
2
|
+
<div class="dbviewer-table-filter-container p-1 mb-0">
|
3
|
+
<i class="bi bi-search dbviewer-table-filter-icon"></i>
|
4
|
+
<input type="text" class="form-control form-control-sm dbviewer-table-filter mb-0"
|
5
|
+
id="tableSearch" placeholder="Filter tables..." aria-label="Filter tables">
|
6
|
+
</div>
|
7
|
+
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<div class="dbviewer-sidebar-content">
|
11
|
+
<% if @tables.any? %>
|
12
|
+
<div class="list-group list-group-flush" id="tablesList">
|
13
|
+
<% @tables.each do |table| %>
|
14
|
+
<%= link_to table_path(table[:name]), title: table[:name],
|
15
|
+
class: "list-group-item list-group-item-action d-flex align-items-center #{'active' if current_table?(table[:name])}",
|
16
|
+
tabindex: "0",
|
17
|
+
data: { table_name: table[:name] },
|
18
|
+
onkeydown: "
|
19
|
+
if(event.key === 'ArrowDown') {
|
20
|
+
event.preventDefault();
|
21
|
+
let next = this.nextElementSibling;
|
22
|
+
while(next && next.classList.contains('d-none')) {
|
23
|
+
next = next.nextElementSibling;
|
24
|
+
}
|
25
|
+
if(next) next.focus();
|
26
|
+
} else if(event.key === 'ArrowUp') {
|
27
|
+
event.preventDefault();
|
28
|
+
let prev = this.previousElementSibling;
|
29
|
+
while(prev && prev.classList.contains('d-none')) {
|
30
|
+
prev = prev.previousElementSibling;
|
31
|
+
}
|
32
|
+
if(prev) prev.focus();
|
33
|
+
else document.getElementById('tableSearch')?.focus();
|
34
|
+
}" do %>
|
35
|
+
<div class="text-truncate">
|
36
|
+
<i class="bi bi-table me-2"></i>
|
37
|
+
<%= format_table_name(table[:name]) %>
|
38
|
+
</div>
|
39
|
+
<div class="ms-auto flex-shrink-0">
|
40
|
+
<span class="badge bg-info" title="Columns"><%= table[:columns_count] %></span>
|
41
|
+
</div>
|
42
|
+
<% end %>
|
43
|
+
<% end %>
|
44
|
+
</div>
|
45
|
+
<% else %>
|
46
|
+
<div class="list-group-item text-muted">
|
47
|
+
<i class="bi bi-exclamation-circle me-2"></i>
|
48
|
+
No tables found
|
49
|
+
</div>
|
50
|
+
<% end %>
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<div class="px-3 py-2 text-muted small">
|
54
|
+
<i class="bi bi-info-circle me-1"></i>
|
55
|
+
<span id="table-count"><%= @tables.size %></span> tables found
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<script>
|
59
|
+
document.addEventListener('DOMContentLoaded', function() {
|
60
|
+
const searchInput = document.getElementById('tableSearch');
|
61
|
+
|
62
|
+
if (searchInput) {
|
63
|
+
// Debounce function to limit how often the filter runs
|
64
|
+
function debounce(func, wait) {
|
65
|
+
let timeout;
|
66
|
+
return function() {
|
67
|
+
const context = this;
|
68
|
+
const args = arguments;
|
69
|
+
clearTimeout(timeout);
|
70
|
+
timeout = setTimeout(function() {
|
71
|
+
func.apply(context, args);
|
72
|
+
}, wait);
|
73
|
+
};
|
74
|
+
}
|
75
|
+
|
76
|
+
// Filter function
|
77
|
+
const filterTables = debounce(function() {
|
78
|
+
const query = searchInput.value.toLowerCase();
|
79
|
+
const tableItems = document.querySelectorAll('#tablesList .list-group-item-action');
|
80
|
+
let visibleCount = 0;
|
81
|
+
|
82
|
+
tableItems.forEach(function(item) {
|
83
|
+
// Get the table name from the title attribute for more accurate matching
|
84
|
+
const tableName = (item.getAttribute('title') || item.textContent).trim().toLowerCase();
|
85
|
+
|
86
|
+
// Also get the displayed text content for a broader match
|
87
|
+
const displayedText = item.textContent.trim().toLowerCase();
|
88
|
+
|
89
|
+
if (tableName.includes(query) || displayedText.includes(query)) {
|
90
|
+
item.classList.remove('d-none');
|
91
|
+
visibleCount++;
|
92
|
+
} else {
|
93
|
+
item.classList.add('d-none');
|
94
|
+
}
|
95
|
+
});
|
96
|
+
|
97
|
+
// Update the tables count in the sidebar
|
98
|
+
const tableCountElement = document.getElementById('table-count');
|
99
|
+
if (tableCountElement) {
|
100
|
+
tableCountElement.textContent = visibleCount;
|
101
|
+
}
|
102
|
+
|
103
|
+
// Show/hide no results message
|
104
|
+
let noResultsEl = document.getElementById('dbviewer-no-filter-results');
|
105
|
+
if (visibleCount === 0 && query !== '') {
|
106
|
+
if (!noResultsEl) {
|
107
|
+
noResultsEl = document.createElement('div');
|
108
|
+
noResultsEl.id = 'dbviewer-no-filter-results';
|
109
|
+
noResultsEl.className = 'list-group-item text-muted text-center py-3';
|
110
|
+
noResultsEl.innerHTML = '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
|
111
|
+
document.getElementById('tablesList').appendChild(noResultsEl);
|
112
|
+
}
|
113
|
+
noResultsEl.querySelector('.fw-bold').textContent = query;
|
114
|
+
noResultsEl.style.display = 'block';
|
115
|
+
} else if (noResultsEl) {
|
116
|
+
noResultsEl.style.display = 'none';
|
117
|
+
}
|
118
|
+
}, 150); // Debounce for 150ms
|
119
|
+
|
120
|
+
// Set up event listeners for the search input
|
121
|
+
searchInput.addEventListener('input', filterTables);
|
122
|
+
searchInput.addEventListener('keyup', function(e) {
|
123
|
+
filterTables();
|
124
|
+
|
125
|
+
// Add keyboard navigation for the filtered list
|
126
|
+
if (e.key === 'Enter' || e.key === 'ArrowDown') {
|
127
|
+
e.preventDefault();
|
128
|
+
// Focus the first visible table item (not having d-none class)
|
129
|
+
const firstVisibleItem = document.querySelector('#tablesList .list-group-item-action:not(.d-none)');
|
130
|
+
if (firstVisibleItem) {
|
131
|
+
firstVisibleItem.focus();
|
132
|
+
// Make sure the item is visible in the scrollable area
|
133
|
+
firstVisibleItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
134
|
+
}
|
135
|
+
}
|
136
|
+
});
|
137
|
+
searchInput.addEventListener('search', filterTables); // For clearing via the "x" in some browsers
|
138
|
+
|
139
|
+
// Clear the search box when clicking the X button
|
140
|
+
const clearButton = document.createElement('button');
|
141
|
+
clearButton.type = 'button';
|
142
|
+
clearButton.className = 'btn btn-sm btn-link position-absolute';
|
143
|
+
clearButton.style.right = '15px';
|
144
|
+
clearButton.style.top = '50%';
|
145
|
+
clearButton.style.transform = 'translateY(-50%)';
|
146
|
+
clearButton.style.display = 'none';
|
147
|
+
clearButton.style.color = '#6c757d';
|
148
|
+
clearButton.style.fontSize = '0.85rem';
|
149
|
+
clearButton.style.padding = '0.25rem';
|
150
|
+
clearButton.style.width = '1.5rem';
|
151
|
+
clearButton.style.textAlign = 'center';
|
152
|
+
clearButton.innerHTML = '<i class="bi bi-x-circle"></i>';
|
153
|
+
clearButton.addEventListener('click', function() {
|
154
|
+
searchInput.value = '';
|
155
|
+
// Call filter directly without debouncing for immediate feedback
|
156
|
+
filterTables();
|
157
|
+
this.style.display = 'none';
|
158
|
+
});
|
159
|
+
|
160
|
+
const filterContainer = document.querySelector('.dbviewer-table-filter-container');
|
161
|
+
if (filterContainer) {
|
162
|
+
filterContainer.style.position = 'relative';
|
163
|
+
filterContainer.appendChild(clearButton);
|
164
|
+
|
165
|
+
searchInput.addEventListener('input', function() {
|
166
|
+
clearButton.style.display = this.value ? 'block' : 'none';
|
167
|
+
});
|
168
|
+
|
169
|
+
// Apply filter initially in case there's a value already (e.g., from browser autofill)
|
170
|
+
if (searchInput.value) {
|
171
|
+
filterTables();
|
172
|
+
clearButton.style.display = 'block';
|
173
|
+
}
|
174
|
+
}
|
175
|
+
}
|
176
|
+
});
|
177
|
+
</script>
|
@@ -0,0 +1,102 @@
|
|
1
|
+
<ul class="nav nav-tabs nav-fill mb-3" id="structureTabs" role="tablist">
|
2
|
+
<li class="nav-item" role="presentation">
|
3
|
+
<button class="nav-link active" id="columns-tab" data-bs-toggle="tab" data-bs-target="#columns" type="button" role="tab" aria-controls="columns" aria-selected="true">
|
4
|
+
<i class="bi bi-grid me-1"></i> Columns
|
5
|
+
</button>
|
6
|
+
</li>
|
7
|
+
<li class="nav-item" role="presentation">
|
8
|
+
<button class="nav-link" id="indexes-tab" data-bs-toggle="tab" data-bs-target="#indexes" type="button" role="tab" aria-controls="indexes" aria-selected="false">
|
9
|
+
<i class="bi bi-search me-1"></i> Indexes
|
10
|
+
</button>
|
11
|
+
</li>
|
12
|
+
<li class="nav-item" role="presentation">
|
13
|
+
<button class="nav-link" id="foreign-keys-tab" data-bs-toggle="tab" data-bs-target="#foreign-keys" type="button" role="tab" aria-controls="foreign-keys" aria-selected="false">
|
14
|
+
<i class="bi bi-link me-1"></i> Foreign Keys
|
15
|
+
</button>
|
16
|
+
</li>
|
17
|
+
</ul>
|
18
|
+
|
19
|
+
<div class="tab-content" id="structureTabContent">
|
20
|
+
<div class="tab-pane fade show active" id="columns" role="tabpanel" aria-labelledby="columns-tab">
|
21
|
+
<div class="table-responsive" style="max-height: 350px; overflow-y: auto;">
|
22
|
+
<table class="table table-sm table-striped">
|
23
|
+
<thead class="sticky-top bg-light">
|
24
|
+
<tr>
|
25
|
+
<th>Column</th>
|
26
|
+
<th>Type</th>
|
27
|
+
<th>Nullable</th>
|
28
|
+
<th>Default</th>
|
29
|
+
<th>Primary Key</th>
|
30
|
+
</tr>
|
31
|
+
</thead>
|
32
|
+
<tbody>
|
33
|
+
<% @columns.each do |column| %>
|
34
|
+
<tr>
|
35
|
+
<td class="fw-medium"><%= column[:name] %></td>
|
36
|
+
<td><span class="badge bg-secondary"><%= column[:type] %></span></td>
|
37
|
+
<td><%= column[:null] ? '<span class="text-success">Yes</span>'.html_safe : '<span class="text-danger">No</span>'.html_safe %></td>
|
38
|
+
<td><code><%= column[:default].nil? ? 'NULL' : column[:default] %></code></td>
|
39
|
+
<td><%= column[:primary] ? '<i class="bi bi-key text-warning"></i> Yes'.html_safe : 'No' %></td>
|
40
|
+
</tr>
|
41
|
+
<% end %>
|
42
|
+
</tbody>
|
43
|
+
</table>
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
|
47
|
+
<div class="tab-pane fade" id="indexes" role="tabpanel" aria-labelledby="indexes-tab">
|
48
|
+
<% if @metadata && @metadata[:indexes].present? %>
|
49
|
+
<div class="table-responsive">
|
50
|
+
<table class="table table-bordered table-striped">
|
51
|
+
<thead>
|
52
|
+
<tr>
|
53
|
+
<th>Name</th>
|
54
|
+
<th>Columns</th>
|
55
|
+
<th>Unique</th>
|
56
|
+
</tr>
|
57
|
+
</thead>
|
58
|
+
<tbody>
|
59
|
+
<% @metadata[:indexes].each do |index| %>
|
60
|
+
<tr>
|
61
|
+
<td><%= index[:name] %></td>
|
62
|
+
<td><%= index[:columns].join(", ") %></td>
|
63
|
+
<td><%= index[:unique] ? 'Yes' : 'No' %></td>
|
64
|
+
</tr>
|
65
|
+
<% end %>
|
66
|
+
</tbody>
|
67
|
+
</table>
|
68
|
+
</div>
|
69
|
+
<% else %>
|
70
|
+
<div class="alert alert-info">No indexes found or not supported by this database.</div>
|
71
|
+
<% end %>
|
72
|
+
</div>
|
73
|
+
|
74
|
+
<div class="tab-pane fade" id="foreign-keys" role="tabpanel" aria-labelledby="foreign-keys-tab">
|
75
|
+
<% if @metadata && @metadata[:foreign_keys].present? %>
|
76
|
+
<div class="table-responsive">
|
77
|
+
<table class="table table-bordered table-striped">
|
78
|
+
<thead>
|
79
|
+
<tr>
|
80
|
+
<th>Name</th>
|
81
|
+
<th>From Column</th>
|
82
|
+
<th>To Table</th>
|
83
|
+
<th>To Column</th>
|
84
|
+
</tr>
|
85
|
+
</thead>
|
86
|
+
<tbody>
|
87
|
+
<% @metadata[:foreign_keys].each do |fk| %>
|
88
|
+
<tr>
|
89
|
+
<td><%= fk[:name] %></td>
|
90
|
+
<td><%= fk[:column] %></td>
|
91
|
+
<td><%= link_to fk[:to_table], table_path(fk[:to_table]) %></td>
|
92
|
+
<td><%= fk[:primary_key] %></td>
|
93
|
+
</tr>
|
94
|
+
<% end %>
|
95
|
+
</tbody>
|
96
|
+
</table>
|
97
|
+
</div>
|
98
|
+
<% else %>
|
99
|
+
<div class="alert alert-info">No foreign keys found or not supported by this database.</div>
|
100
|
+
<% end %>
|
101
|
+
</div>
|
102
|
+
</div>
|
@@ -0,0 +1,128 @@
|
|
1
|
+
<% content_for :title do %>
|
2
|
+
Database Tables
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<% content_for :sidebar do %>
|
6
|
+
<%= render 'dbviewer/shared/sidebar' %>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
10
|
+
<h1>Database Tables</h1>
|
11
|
+
<div>
|
12
|
+
<%= link_to dashboard_path, class: "btn btn-outline-primary me-2" do %>
|
13
|
+
<i class="bi bi-house-door me-1"></i> Dashboard
|
14
|
+
<% end %>
|
15
|
+
<%= link_to entity_relationship_diagrams_path, class: "btn btn-outline-primary" do %>
|
16
|
+
<i class="bi bi-diagram-3 me-1"></i> View ERD
|
17
|
+
<% end %>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
|
21
|
+
<% if flash[:error] %>
|
22
|
+
<div class="alert alert-danger" role="alert">
|
23
|
+
<%= flash[:error] %>
|
24
|
+
</div>
|
25
|
+
<% end %>
|
26
|
+
|
27
|
+
<!-- Table Listing -->
|
28
|
+
<div class="card shadow-sm mb-4">
|
29
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
30
|
+
<h5 class="mb-0">All Tables</h5>
|
31
|
+
<span class="badge bg-secondary"><%= @tables.size %> tables</span>
|
32
|
+
</div>
|
33
|
+
<div class="card-body p-0">
|
34
|
+
<div class="table-responsive">
|
35
|
+
<table class="table table-hover table-striped mb-0">
|
36
|
+
<thead>
|
37
|
+
<tr>
|
38
|
+
<th>Table Name</th>
|
39
|
+
<th class="text-center">Columns</th>
|
40
|
+
<th>Actions</th>
|
41
|
+
</tr>
|
42
|
+
</thead>
|
43
|
+
<tbody>
|
44
|
+
<% @tables.each do |table| %>
|
45
|
+
<tr>
|
46
|
+
<td class="fw-medium">
|
47
|
+
<%= link_to table[:name], table_path(table[:name]), class: "text-decoration-none" %>
|
48
|
+
</td>
|
49
|
+
<td class="text-center">
|
50
|
+
<span class="badge bg-secondary-subtle"><%= table[:columns_count] %></span>
|
51
|
+
</td>
|
52
|
+
<td>
|
53
|
+
<%= link_to raw('<i class="bi bi-eye"></i>'), table_path(table[:name]), class: "btn btn-sm btn-outline-primary", title: "View" %>
|
54
|
+
<%= link_to raw('<i class="bi bi-search"></i>'), query_table_path(table[:name]), class: "btn btn-sm btn-outline-secondary", title: "Query" %>
|
55
|
+
<%= link_to raw('<i class="bi bi-download"></i>'), export_csv_table_path(table[:name]), class: "btn btn-sm btn-outline-success", title: "Export CSV" %>
|
56
|
+
</td>
|
57
|
+
</tr>
|
58
|
+
<% end %>
|
59
|
+
</tbody>
|
60
|
+
</table>
|
61
|
+
</div>
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
|
65
|
+
<script>
|
66
|
+
document.addEventListener('DOMContentLoaded', function() {
|
67
|
+
// Table search functionality
|
68
|
+
const searchInput = document.getElementById('tableSearch');
|
69
|
+
if (searchInput) {
|
70
|
+
searchInput.addEventListener('keyup', function() {
|
71
|
+
const filter = this.value.toLowerCase();
|
72
|
+
const tableRows = document.querySelectorAll('tbody tr');
|
73
|
+
|
74
|
+
tableRows.forEach(function(row) {
|
75
|
+
const tableName = row.querySelector('td:first-child').textContent.toLowerCase();
|
76
|
+
if (tableName.includes(filter)) {
|
77
|
+
row.style.display = '';
|
78
|
+
} else {
|
79
|
+
row.style.display = 'none';
|
80
|
+
}
|
81
|
+
});
|
82
|
+
});
|
83
|
+
}
|
84
|
+
|
85
|
+
// Update table styling when theme changes
|
86
|
+
function updateTableStyling() {
|
87
|
+
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
|
88
|
+
const tables = document.querySelectorAll('.table');
|
89
|
+
|
90
|
+
tables.forEach(table => {
|
91
|
+
if (isDarkMode) {
|
92
|
+
table.classList.add('table-dark');
|
93
|
+
} else {
|
94
|
+
table.classList.remove('table-dark');
|
95
|
+
}
|
96
|
+
});
|
97
|
+
}
|
98
|
+
|
99
|
+
// Initial styling
|
100
|
+
updateTableStyling();
|
101
|
+
|
102
|
+
// Listen for theme changes
|
103
|
+
document.addEventListener('dbviewerThemeChanged', function(event) {
|
104
|
+
updateTableStyling();
|
105
|
+
});
|
106
|
+
});
|
107
|
+
</script>
|
108
|
+
|
109
|
+
<style>
|
110
|
+
/* Dark mode table styles */
|
111
|
+
[data-bs-theme="dark"] .table {
|
112
|
+
--bs-table-striped-bg: rgba(255, 255, 255, 0.05);
|
113
|
+
--bs-table-hover-bg: rgba(255, 255, 255, 0.075);
|
114
|
+
}
|
115
|
+
|
116
|
+
/* Fix badge styling for dark mode */
|
117
|
+
[data-bs-theme="dark"] .bg-secondary-subtle {
|
118
|
+
background-color: rgba(255, 255, 255, 0.15) !important;
|
119
|
+
color: #e9ecef !important;
|
120
|
+
}
|
121
|
+
|
122
|
+
[data-bs-theme="light"] .bg-secondary-subtle {
|
123
|
+
background-color: #e9ecef !important;
|
124
|
+
color: #212529 !important;
|
125
|
+
}
|
126
|
+
</style>
|
127
|
+
|
128
|
+
|