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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +250 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/dbviewer/application.css +21 -0
  6. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  7. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  8. data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
  9. data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
  10. data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
  11. data/app/controllers/dbviewer/application_controller.rb +21 -0
  12. data/app/controllers/dbviewer/databases_controller.rb +0 -0
  13. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
  14. data/app/controllers/dbviewer/home_controller.rb +10 -0
  15. data/app/controllers/dbviewer/logs_controller.rb +39 -0
  16. data/app/controllers/dbviewer/tables_controller.rb +73 -0
  17. data/app/helpers/dbviewer/application_helper.rb +118 -0
  18. data/app/jobs/dbviewer/application_job.rb +4 -0
  19. data/app/mailers/dbviewer/application_mailer.rb +6 -0
  20. data/app/models/dbviewer/application_record.rb +5 -0
  21. data/app/services/dbviewer/file_storage.rb +0 -0
  22. data/app/services/dbviewer/in_memory_storage.rb +0 -0
  23. data/app/services/dbviewer/query_analyzer.rb +0 -0
  24. data/app/services/dbviewer/query_collection.rb +0 -0
  25. data/app/services/dbviewer/query_logger.rb +0 -0
  26. data/app/services/dbviewer/query_parser.rb +82 -0
  27. data/app/services/dbviewer/query_storage.rb +0 -0
  28. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
  29. data/app/views/dbviewer/home/index.html.erb +237 -0
  30. data/app/views/dbviewer/logs/index.html.erb +614 -0
  31. data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
  32. data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
  33. data/app/views/dbviewer/tables/index.html.erb +128 -0
  34. data/app/views/dbviewer/tables/query.html.erb +600 -0
  35. data/app/views/dbviewer/tables/show.html.erb +271 -0
  36. data/app/views/layouts/dbviewer/application.html.erb +728 -0
  37. data/config/routes.rb +22 -0
  38. data/lib/dbviewer/configuration.rb +79 -0
  39. data/lib/dbviewer/database_manager.rb +450 -0
  40. data/lib/dbviewer/engine.rb +20 -0
  41. data/lib/dbviewer/initializer.rb +23 -0
  42. data/lib/dbviewer/logger.rb +102 -0
  43. data/lib/dbviewer/query_analyzer.rb +109 -0
  44. data/lib/dbviewer/query_collection.rb +41 -0
  45. data/lib/dbviewer/query_parser.rb +82 -0
  46. data/lib/dbviewer/sql_validator.rb +194 -0
  47. data/lib/dbviewer/storage/base.rb +31 -0
  48. data/lib/dbviewer/storage/file_storage.rb +96 -0
  49. data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
  50. data/lib/dbviewer/version.rb +3 -0
  51. data/lib/dbviewer.rb +65 -0
  52. data/lib/tasks/dbviewer_tasks.rake +4 -0
  53. 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
+