dbviewer 0.7.1 → 0.7.3
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 +4 -4
- data/README.md +7 -33
- data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +53 -49
- data/app/assets/javascripts/dbviewer/sidebar.js +210 -0
- data/app/controllers/dbviewer/home_controller.rb +0 -6
- data/app/views/dbviewer/shared/_tables_sidebar.html.erb +1 -17
- data/app/views/layouts/dbviewer/application.html.erb +1 -0
- data/app/views/layouts/dbviewer/shared/_sidebar.html.erb +0 -243
- data/lib/dbviewer/database/dynamic_model_factory.rb +16 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50301b7fa1867ac7e266e3b48eb962e7e456de1e43a664750e8885a9b603f6ca
|
4
|
+
data.tar.gz: 0377f20859609a2807bdecc8bc5f9118dae6581b7d4819504900e7e6a371dba3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b17042fe0172fba83874c2498a99e9b9681092c5aedefb495cd2d55a41913d2d84dbbbdac3daa03a59928d465d7538183aa3dc5f12e41fda54add729b8963291
|
7
|
+
data.tar.gz: e3163cce763297208aeccae790ee6fb9890a2f084a974e2ac3e41acc0dac51ea6d005409cc57c71e3e605d1d406e7ea95b1ae9b83647800ae6d26d1444b8083e
|
data/README.md
CHANGED
@@ -37,7 +37,7 @@ You can explore a live demo of DBViewer at [https://dbviewer-demo.wailantirajoh.
|
|
37
37
|
Add this line to your application's Gemfile:
|
38
38
|
|
39
39
|
```ruby
|
40
|
-
gem "dbviewer"
|
40
|
+
gem "dbviewer"
|
41
41
|
```
|
42
42
|
|
43
43
|
And then execute:
|
@@ -151,9 +151,11 @@ end
|
|
151
151
|
|
152
152
|
Each connection needs to reference an ActiveRecord class that establishes a database connection. For more details, see [Multiple Database Connections](docs/multiple_connections.md).
|
153
153
|
|
154
|
-
## 🪵 Query Logging
|
154
|
+
## 🪵 Query Logging (Development Only)
|
155
155
|
|
156
|
-
DBViewer includes a powerful SQL query logging system that captures and analyzes database queries. You can access this log through the `/dbviewer/logs` endpoint.
|
156
|
+
DBViewer includes a powerful SQL query logging system that captures and analyzes database queries. You can access this log through the `/dbviewer/logs` endpoint.
|
157
|
+
|
158
|
+
The logging system offers two storage backends:
|
157
159
|
|
158
160
|
### Disabling Query Logging
|
159
161
|
|
@@ -210,34 +212,6 @@ end
|
|
210
212
|
When credentials are provided, all DBViewer routes will be protected by HTTP Basic Authentication.
|
211
213
|
Without valid credentials, users will be prompted for a username and password before they can access any DBViewer page.
|
212
214
|
|
213
|
-
## 🌱 Production Access
|
214
|
-
|
215
|
-
With the addition of Basic Authentication, DBViewer can now be used in any environment including production. We recommend the following for production deployments:
|
216
|
-
|
217
|
-
1. **Always** enable HTTP Basic Authentication with strong credentials:
|
218
|
-
|
219
|
-
```ruby
|
220
|
-
Dbviewer.configure do |config|
|
221
|
-
config.admin_credentials = {
|
222
|
-
username: "unique_username",
|
223
|
-
password: SecureRandom.hex(16) # Generate a strong random password
|
224
|
-
}
|
225
|
-
end
|
226
|
-
```
|
227
|
-
|
228
|
-
2. Mount the engine in your routes file:
|
229
|
-
|
230
|
-
```ruby
|
231
|
-
# In any environment, with Basic Auth protection
|
232
|
-
mount Dbviewer::Engine, at: "/dbviewer"
|
233
|
-
```
|
234
|
-
|
235
|
-
3. Access the tool through your regular application URL:
|
236
|
-
|
237
|
-
```
|
238
|
-
https://yourdomain.com/dbviewer
|
239
|
-
```
|
240
|
-
|
241
215
|
## 📝 Security Note
|
242
216
|
|
243
217
|
⚠️ **Warning**: This engine provides direct access to your database contents, which contains sensitive information. Always protect it with HTTP Basic Authentication by configuring strong credentials as shown above.
|
@@ -260,10 +234,10 @@ The simplest way to update is using Bundler:
|
|
260
234
|
|
261
235
|
```ruby
|
262
236
|
# For the latest version
|
263
|
-
gem "dbviewer"
|
237
|
+
gem "dbviewer"
|
264
238
|
|
265
239
|
# Or specify a version
|
266
|
-
gem "dbviewer", "~> 0.
|
240
|
+
gem "dbviewer", "~> 0.7.2"
|
267
241
|
```
|
268
242
|
|
269
243
|
- Run bundle update:
|
@@ -100,37 +100,35 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
100
100
|
let relationshipsLoaded = false;
|
101
101
|
|
102
102
|
// Function to fetch relationships asynchronously
|
103
|
-
function fetchRelationships() {
|
103
|
+
async function fetchRelationships() {
|
104
104
|
const apiPath = document.getElementById("relationships_api_path").value;
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
.then((response) => {
|
112
|
-
if (!response.ok) {
|
113
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
114
|
-
}
|
115
|
-
return response.json();
|
116
|
-
})
|
117
|
-
.then((data) => {
|
118
|
-
relationships = data.relationships || [];
|
119
|
-
relationshipsLoaded = true;
|
120
|
-
updateRelationshipsStatus(true);
|
121
|
-
return relationships;
|
122
|
-
})
|
123
|
-
.catch((error) => {
|
124
|
-
console.error("Error fetching relationships:", error);
|
125
|
-
relationshipsLoaded = true; // Mark as loaded even on error to prevent infinite loading
|
126
|
-
updateRelationshipsStatus(true);
|
127
|
-
return [];
|
105
|
+
try {
|
106
|
+
const response = await fetch(apiPath, {
|
107
|
+
headers: {
|
108
|
+
Accept: "application/json",
|
109
|
+
"X-Requested-With": "XMLHttpRequest",
|
110
|
+
},
|
128
111
|
});
|
112
|
+
|
113
|
+
if (!response.ok) {
|
114
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
115
|
+
}
|
116
|
+
|
117
|
+
const data = await response.json();
|
118
|
+
relationships = data.relationships || [];
|
119
|
+
relationshipsLoaded = true;
|
120
|
+
updateRelationshipsStatus(true);
|
121
|
+
return relationships;
|
122
|
+
} catch (error) {
|
123
|
+
console.error("Error fetching relationships:", error);
|
124
|
+
relationshipsLoaded = true; // Mark as loaded even on error to prevent infinite loading
|
125
|
+
updateRelationshipsStatus(true);
|
126
|
+
return [];
|
127
|
+
}
|
129
128
|
}
|
130
129
|
|
131
130
|
// Function to update loading status
|
132
131
|
function updateLoadingStatus(message) {
|
133
|
-
const loadingElement = document.getElementById("erd-loading");
|
134
132
|
const loadingPhase = document.getElementById("loading-phase");
|
135
133
|
if (loadingPhase) {
|
136
134
|
loadingPhase.textContent = message;
|
@@ -196,6 +194,35 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
196
194
|
const tablePath = document.getElementById("tables_path").value;
|
197
195
|
|
198
196
|
// First pass: add all tables with minimal info and start loading columns
|
197
|
+
// Function to fetch column data for a table
|
198
|
+
async function fetchTableColumns(tableName) {
|
199
|
+
try {
|
200
|
+
const response = await fetch(`${tablePath}/${tableName}?format=json`, {
|
201
|
+
headers: {
|
202
|
+
Accept: "application/json",
|
203
|
+
"X-Requested-With": "XMLHttpRequest",
|
204
|
+
},
|
205
|
+
});
|
206
|
+
|
207
|
+
const data = await response.json();
|
208
|
+
|
209
|
+
if (data && data.columns) {
|
210
|
+
tableColumns[tableName] = data.columns;
|
211
|
+
columnsLoadedCount++;
|
212
|
+
|
213
|
+
// Update progress bar
|
214
|
+
updateTableProgress(columnsLoadedCount, totalTables);
|
215
|
+
|
216
|
+
checkIfReadyToUpdate();
|
217
|
+
}
|
218
|
+
} catch (error) {
|
219
|
+
console.error(`Error fetching columns for table ${tableName}:`, error);
|
220
|
+
columnsLoadedCount++;
|
221
|
+
updateTableProgress(columnsLoadedCount, totalTables);
|
222
|
+
checkIfReadyToUpdate();
|
223
|
+
}
|
224
|
+
}
|
225
|
+
|
199
226
|
tables.forEach(function (table) {
|
200
227
|
const tableName = table.name;
|
201
228
|
mermaidDefinition += ` ${tableName} {\n`;
|
@@ -203,30 +230,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
203
230
|
mermaidDefinition += " }\n";
|
204
231
|
|
205
232
|
// Start loading column data asynchronously
|
206
|
-
|
207
|
-
headers: {
|
208
|
-
Accept: "application/json",
|
209
|
-
"X-Requested-With": "XMLHttpRequest",
|
210
|
-
},
|
211
|
-
})
|
212
|
-
.then((response) => response.json())
|
213
|
-
.then((data) => {
|
214
|
-
if (data && data.columns) {
|
215
|
-
tableColumns[tableName] = data.columns;
|
216
|
-
columnsLoadedCount++;
|
217
|
-
|
218
|
-
// Update progress bar
|
219
|
-
updateTableProgress(columnsLoadedCount, totalTables);
|
220
|
-
|
221
|
-
checkIfReadyToUpdate();
|
222
|
-
}
|
223
|
-
})
|
224
|
-
.catch((error) => {
|
225
|
-
console.error(`Error fetching columns for table ${tableName}:`, error);
|
226
|
-
columnsLoadedCount++;
|
227
|
-
updateTableProgress(columnsLoadedCount, totalTables);
|
228
|
-
checkIfReadyToUpdate();
|
229
|
-
});
|
233
|
+
fetchTableColumns(tableName);
|
230
234
|
});
|
231
235
|
|
232
236
|
// Function to check if we're ready to update the diagram with full data
|
@@ -0,0 +1,210 @@
|
|
1
|
+
document.addEventListener("DOMContentLoaded", function () {
|
2
|
+
const searchInput = document.getElementById("tableSearch");
|
3
|
+
const sidebarContent = document.querySelector(".dbviewer-sidebar-content");
|
4
|
+
|
5
|
+
// Storage keys for persistence
|
6
|
+
const STORAGE_KEYS = {
|
7
|
+
searchFilter: "dbviewer_sidebar_search_filter",
|
8
|
+
scrollPosition: "dbviewer_sidebar_scroll_position",
|
9
|
+
};
|
10
|
+
|
11
|
+
if (searchInput) {
|
12
|
+
// Debounce function to limit how often the filter runs
|
13
|
+
function debounce(func, wait) {
|
14
|
+
let timeout;
|
15
|
+
return function () {
|
16
|
+
const context = this;
|
17
|
+
const args = arguments;
|
18
|
+
clearTimeout(timeout);
|
19
|
+
timeout = setTimeout(function () {
|
20
|
+
func.apply(context, args);
|
21
|
+
}, wait);
|
22
|
+
};
|
23
|
+
}
|
24
|
+
|
25
|
+
// Filter function
|
26
|
+
const filterTables = debounce(function () {
|
27
|
+
const query = searchInput.value.toLowerCase();
|
28
|
+
const tableItems = document.querySelectorAll(
|
29
|
+
"#tablesList .list-group-item-action"
|
30
|
+
);
|
31
|
+
let visibleCount = 0;
|
32
|
+
|
33
|
+
// Save the current search filter to localStorage
|
34
|
+
localStorage.setItem(STORAGE_KEYS.searchFilter, searchInput.value);
|
35
|
+
|
36
|
+
tableItems.forEach(function (item) {
|
37
|
+
// Get the table name from the title attribute for more accurate matching
|
38
|
+
const tableName = (item.getAttribute("title") || item.textContent)
|
39
|
+
.trim()
|
40
|
+
.toLowerCase();
|
41
|
+
|
42
|
+
// Also get the displayed text content for a broader match
|
43
|
+
const displayedText = item.textContent.trim().toLowerCase();
|
44
|
+
|
45
|
+
if (tableName.includes(query) || displayedText.includes(query)) {
|
46
|
+
item.classList.remove("d-none");
|
47
|
+
visibleCount++;
|
48
|
+
} else {
|
49
|
+
item.classList.add("d-none");
|
50
|
+
}
|
51
|
+
});
|
52
|
+
|
53
|
+
// Update the tables count in the sidebar
|
54
|
+
const tableCountElement = document.getElementById("table-count");
|
55
|
+
if (tableCountElement) {
|
56
|
+
tableCountElement.textContent = visibleCount;
|
57
|
+
}
|
58
|
+
|
59
|
+
// Show/hide no results message
|
60
|
+
let noResultsEl = document.getElementById("dbviewer-no-filter-results");
|
61
|
+
if (visibleCount === 0 && query !== "") {
|
62
|
+
if (!noResultsEl) {
|
63
|
+
noResultsEl = document.createElement("div");
|
64
|
+
noResultsEl.id = "dbviewer-no-filter-results";
|
65
|
+
noResultsEl.className = "list-group-item text-muted text-center py-3";
|
66
|
+
noResultsEl.innerHTML =
|
67
|
+
'<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
|
68
|
+
document.getElementById("tablesList").appendChild(noResultsEl);
|
69
|
+
}
|
70
|
+
noResultsEl.querySelector(".fw-bold").textContent = query;
|
71
|
+
noResultsEl.style.display = "block";
|
72
|
+
} else if (noResultsEl) {
|
73
|
+
noResultsEl.style.display = "none";
|
74
|
+
}
|
75
|
+
}, 150); // Debounce for 150ms
|
76
|
+
|
77
|
+
// Set up clear button first
|
78
|
+
const clearButton = document.createElement("button");
|
79
|
+
clearButton.type = "button";
|
80
|
+
clearButton.className = "btn btn-sm btn-link position-absolute";
|
81
|
+
clearButton.style.right = "15px";
|
82
|
+
clearButton.style.top = "50%";
|
83
|
+
clearButton.style.transform = "translateY(-50%)";
|
84
|
+
clearButton.style.display = "none";
|
85
|
+
clearButton.style.color = "#6c757d";
|
86
|
+
clearButton.style.fontSize = "0.85rem";
|
87
|
+
clearButton.style.padding = "0.25rem";
|
88
|
+
clearButton.style.width = "1.5rem";
|
89
|
+
clearButton.style.textAlign = "center";
|
90
|
+
clearButton.innerHTML = '<i class="bi bi-x-circle"></i>';
|
91
|
+
clearButton.addEventListener("click", function () {
|
92
|
+
searchInput.value = "";
|
93
|
+
// Clear the saved filter from localStorage
|
94
|
+
localStorage.removeItem(STORAGE_KEYS.searchFilter);
|
95
|
+
// Call filter directly without debouncing for immediate feedback
|
96
|
+
filterTables();
|
97
|
+
this.style.display = "none";
|
98
|
+
});
|
99
|
+
|
100
|
+
const filterContainer = document.querySelector(
|
101
|
+
".dbviewer-table-filter-container"
|
102
|
+
);
|
103
|
+
if (filterContainer) {
|
104
|
+
filterContainer.style.position = "relative";
|
105
|
+
filterContainer.appendChild(clearButton);
|
106
|
+
|
107
|
+
searchInput.addEventListener("input", function () {
|
108
|
+
clearButton.style.display = this.value ? "block" : "none";
|
109
|
+
});
|
110
|
+
}
|
111
|
+
|
112
|
+
// Restore saved search filter on page load and apply it immediately
|
113
|
+
const savedFilter = localStorage.getItem(STORAGE_KEYS.searchFilter);
|
114
|
+
if (savedFilter) {
|
115
|
+
searchInput.value = savedFilter;
|
116
|
+
// Show clear button immediately when filter is restored
|
117
|
+
clearButton.style.display = "block";
|
118
|
+
// Apply filter immediately without debouncing to prevent blinking
|
119
|
+
const query = savedFilter.toLowerCase();
|
120
|
+
const tableItems = document.querySelectorAll(
|
121
|
+
"#tablesList .list-group-item-action"
|
122
|
+
);
|
123
|
+
let visibleCount = 0;
|
124
|
+
|
125
|
+
tableItems.forEach(function (item) {
|
126
|
+
const tableName = (item.getAttribute("title") || item.textContent)
|
127
|
+
.trim()
|
128
|
+
.toLowerCase();
|
129
|
+
const displayedText = item.textContent.trim().toLowerCase();
|
130
|
+
|
131
|
+
if (tableName.includes(query) || displayedText.includes(query)) {
|
132
|
+
item.classList.remove("d-none");
|
133
|
+
visibleCount++;
|
134
|
+
} else {
|
135
|
+
item.classList.add("d-none");
|
136
|
+
}
|
137
|
+
});
|
138
|
+
|
139
|
+
// Update the tables count immediately
|
140
|
+
const tableCountElement = document.getElementById("table-count");
|
141
|
+
if (tableCountElement) {
|
142
|
+
tableCountElement.textContent = visibleCount;
|
143
|
+
}
|
144
|
+
|
145
|
+
// Handle no results message immediately
|
146
|
+
let noResultsEl = document.getElementById("dbviewer-no-filter-results");
|
147
|
+
if (visibleCount === 0 && query !== "") {
|
148
|
+
if (!noResultsEl) {
|
149
|
+
noResultsEl = document.createElement("div");
|
150
|
+
noResultsEl.id = "dbviewer-no-filter-results";
|
151
|
+
noResultsEl.className = "list-group-item text-muted text-center py-3";
|
152
|
+
noResultsEl.innerHTML =
|
153
|
+
'<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
|
154
|
+
document.getElementById("tablesList").appendChild(noResultsEl);
|
155
|
+
}
|
156
|
+
noResultsEl.querySelector(".fw-bold").textContent = query;
|
157
|
+
noResultsEl.style.display = "block";
|
158
|
+
} else if (noResultsEl) {
|
159
|
+
noResultsEl.style.display = "none";
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
// Restore saved scroll position on page load
|
164
|
+
if (sidebarContent) {
|
165
|
+
const savedScrollPosition = localStorage.getItem(
|
166
|
+
STORAGE_KEYS.scrollPosition
|
167
|
+
);
|
168
|
+
if (savedScrollPosition) {
|
169
|
+
// Use requestAnimationFrame to ensure DOM is fully rendered
|
170
|
+
requestAnimationFrame(() => {
|
171
|
+
sidebarContent.scrollTop = parseInt(savedScrollPosition, 10);
|
172
|
+
});
|
173
|
+
}
|
174
|
+
|
175
|
+
// Save scroll position on scroll
|
176
|
+
const saveScrollPosition = debounce(function () {
|
177
|
+
localStorage.setItem(
|
178
|
+
STORAGE_KEYS.scrollPosition,
|
179
|
+
sidebarContent.scrollTop
|
180
|
+
);
|
181
|
+
}, 100);
|
182
|
+
|
183
|
+
sidebarContent.addEventListener("scroll", saveScrollPosition);
|
184
|
+
}
|
185
|
+
|
186
|
+
// Set up event listeners for the search input
|
187
|
+
searchInput.addEventListener("input", filterTables);
|
188
|
+
searchInput.addEventListener("keyup", function (e) {
|
189
|
+
filterTables();
|
190
|
+
|
191
|
+
// Add keyboard navigation for the filtered list
|
192
|
+
if (e.key === "Enter" || e.key === "ArrowDown") {
|
193
|
+
e.preventDefault();
|
194
|
+
// Focus the first visible table item (not having d-none class)
|
195
|
+
const firstVisibleItem = document.querySelector(
|
196
|
+
"#tablesList .list-group-item-action:not(.d-none)"
|
197
|
+
);
|
198
|
+
if (firstVisibleItem) {
|
199
|
+
firstVisibleItem.focus();
|
200
|
+
// Make sure the item is visible in the scrollable area
|
201
|
+
firstVisibleItem.scrollIntoView({
|
202
|
+
behavior: "smooth",
|
203
|
+
block: "nearest",
|
204
|
+
});
|
205
|
+
}
|
206
|
+
}
|
207
|
+
});
|
208
|
+
searchInput.addEventListener("search", filterTables); // For clearing via the "x" in some browsers
|
209
|
+
}
|
210
|
+
});
|
@@ -11,23 +11,7 @@
|
|
11
11
|
title: table[:name],
|
12
12
|
class: "list-group-item list-group-item-action d-flex align-items-center #{'active' if current_table?(table[:name])}",
|
13
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 %>
|
14
|
+
data: { table_name: table[:name] } do %>
|
31
15
|
<div class="d-flex justify-content-between align-items-center w-100">
|
32
16
|
<div class="text-truncate">
|
33
17
|
<i class="bi bi-table me-2 small"></i>
|
@@ -38,6 +38,7 @@
|
|
38
38
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
39
39
|
|
40
40
|
<%= javascript_include_tag "dbviewer/layout", "data-turbo-track": "reload" %>
|
41
|
+
<%= javascript_include_tag "dbviewer/sidebar", "data-turbo-track": "reload" %>
|
41
42
|
<%= stylesheet_link_tag "dbviewer/application", "data-turbo-track": "reload" %>
|
42
43
|
|
43
44
|
<%= yield :head %>
|
@@ -3,58 +3,6 @@
|
|
3
3
|
<input type="text" class="form-control form-control-sm dbviewer-table-filter mb-0"
|
4
4
|
id="tableSearch" placeholder="Filter tables..." aria-label="Filter tables">
|
5
5
|
</div>
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
<!-- Add custom styling for datetime inputs -->
|
10
|
-
<style>
|
11
|
-
/* Better datetime input styling */
|
12
|
-
input[type="datetime-local"] {
|
13
|
-
padding-right: 0.5rem;
|
14
|
-
}
|
15
|
-
|
16
|
-
/* Dark mode support for datetime inputs */
|
17
|
-
[data-bs-theme="dark"] input[type="datetime-local"] {
|
18
|
-
background-color: rgba(255,255,255,0.1);
|
19
|
-
color: #fff;
|
20
|
-
border-color: rgba(255,255,255,0.15);
|
21
|
-
}
|
22
|
-
|
23
|
-
[data-bs-theme="dark"] input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
24
|
-
filter: invert(1);
|
25
|
-
}
|
26
|
-
</style>
|
27
|
-
|
28
|
-
<script>
|
29
|
-
// Set default values for datetime inputs when empty
|
30
|
-
document.addEventListener('DOMContentLoaded', function() {
|
31
|
-
const startInput = document.getElementById('creationFilterStart');
|
32
|
-
const endInput = document.getElementById('creationFilterEnd');
|
33
|
-
|
34
|
-
// When applying filter with empty start date, default to beginning of current month
|
35
|
-
if (startInput) {
|
36
|
-
startInput.addEventListener('click', function() {
|
37
|
-
if (!this.value) {
|
38
|
-
const now = new Date();
|
39
|
-
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
40
|
-
const formattedDate = firstDay.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:MM
|
41
|
-
this.value = formattedDate;
|
42
|
-
}
|
43
|
-
});
|
44
|
-
}
|
45
|
-
|
46
|
-
// When applying filter with empty end date, default to current datetime
|
47
|
-
if (endInput) {
|
48
|
-
endInput.addEventListener('click', function() {
|
49
|
-
if (!this.value) {
|
50
|
-
const now = new Date();
|
51
|
-
const formattedDate = now.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:MM
|
52
|
-
this.value = formattedDate;
|
53
|
-
}
|
54
|
-
});
|
55
|
-
}
|
56
|
-
});
|
57
|
-
</script>
|
58
6
|
</div>
|
59
7
|
|
60
8
|
<div class="dbviewer-sidebar-content">
|
@@ -115,194 +63,3 @@
|
|
115
63
|
<i class="bi bi-gem me-1"></i>
|
116
64
|
DBViewer v<%= Dbviewer::VERSION %>
|
117
65
|
</div>
|
118
|
-
|
119
|
-
<script>
|
120
|
-
document.addEventListener('DOMContentLoaded', function() {
|
121
|
-
const searchInput = document.getElementById('tableSearch');
|
122
|
-
const sidebarContent = document.querySelector('.dbviewer-sidebar-content');
|
123
|
-
|
124
|
-
// Storage keys for persistence
|
125
|
-
const STORAGE_KEYS = {
|
126
|
-
searchFilter: 'dbviewer_sidebar_search_filter',
|
127
|
-
scrollPosition: 'dbviewer_sidebar_scroll_position'
|
128
|
-
};
|
129
|
-
|
130
|
-
if (searchInput) {
|
131
|
-
// Debounce function to limit how often the filter runs
|
132
|
-
function debounce(func, wait) {
|
133
|
-
let timeout;
|
134
|
-
return function() {
|
135
|
-
const context = this;
|
136
|
-
const args = arguments;
|
137
|
-
clearTimeout(timeout);
|
138
|
-
timeout = setTimeout(function() {
|
139
|
-
func.apply(context, args);
|
140
|
-
}, wait);
|
141
|
-
};
|
142
|
-
}
|
143
|
-
|
144
|
-
// Filter function
|
145
|
-
const filterTables = debounce(function() {
|
146
|
-
const query = searchInput.value.toLowerCase();
|
147
|
-
const tableItems = document.querySelectorAll('#tablesList .list-group-item-action');
|
148
|
-
let visibleCount = 0;
|
149
|
-
|
150
|
-
// Save the current search filter to localStorage
|
151
|
-
localStorage.setItem(STORAGE_KEYS.searchFilter, searchInput.value);
|
152
|
-
|
153
|
-
tableItems.forEach(function(item) {
|
154
|
-
// Get the table name from the title attribute for more accurate matching
|
155
|
-
const tableName = (item.getAttribute('title') || item.textContent).trim().toLowerCase();
|
156
|
-
|
157
|
-
// Also get the displayed text content for a broader match
|
158
|
-
const displayedText = item.textContent.trim().toLowerCase();
|
159
|
-
|
160
|
-
if (tableName.includes(query) || displayedText.includes(query)) {
|
161
|
-
item.classList.remove('d-none');
|
162
|
-
visibleCount++;
|
163
|
-
} else {
|
164
|
-
item.classList.add('d-none');
|
165
|
-
}
|
166
|
-
});
|
167
|
-
|
168
|
-
// Update the tables count in the sidebar
|
169
|
-
const tableCountElement = document.getElementById('table-count');
|
170
|
-
if (tableCountElement) {
|
171
|
-
tableCountElement.textContent = visibleCount;
|
172
|
-
}
|
173
|
-
|
174
|
-
// Show/hide no results message
|
175
|
-
let noResultsEl = document.getElementById('dbviewer-no-filter-results');
|
176
|
-
if (visibleCount === 0 && query !== '') {
|
177
|
-
if (!noResultsEl) {
|
178
|
-
noResultsEl = document.createElement('div');
|
179
|
-
noResultsEl.id = 'dbviewer-no-filter-results';
|
180
|
-
noResultsEl.className = 'list-group-item text-muted text-center py-3';
|
181
|
-
noResultsEl.innerHTML = '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
|
182
|
-
document.getElementById('tablesList').appendChild(noResultsEl);
|
183
|
-
}
|
184
|
-
noResultsEl.querySelector('.fw-bold').textContent = query;
|
185
|
-
noResultsEl.style.display = 'block';
|
186
|
-
} else if (noResultsEl) {
|
187
|
-
noResultsEl.style.display = 'none';
|
188
|
-
}
|
189
|
-
}, 150); // Debounce for 150ms
|
190
|
-
|
191
|
-
// Set up clear button first
|
192
|
-
const clearButton = document.createElement('button');
|
193
|
-
clearButton.type = 'button';
|
194
|
-
clearButton.className = 'btn btn-sm btn-link position-absolute';
|
195
|
-
clearButton.style.right = '15px';
|
196
|
-
clearButton.style.top = '50%';
|
197
|
-
clearButton.style.transform = 'translateY(-50%)';
|
198
|
-
clearButton.style.display = 'none';
|
199
|
-
clearButton.style.color = '#6c757d';
|
200
|
-
clearButton.style.fontSize = '0.85rem';
|
201
|
-
clearButton.style.padding = '0.25rem';
|
202
|
-
clearButton.style.width = '1.5rem';
|
203
|
-
clearButton.style.textAlign = 'center';
|
204
|
-
clearButton.innerHTML = '<i class="bi bi-x-circle"></i>';
|
205
|
-
clearButton.addEventListener('click', function() {
|
206
|
-
searchInput.value = '';
|
207
|
-
// Clear the saved filter from localStorage
|
208
|
-
localStorage.removeItem(STORAGE_KEYS.searchFilter);
|
209
|
-
// Call filter directly without debouncing for immediate feedback
|
210
|
-
filterTables();
|
211
|
-
this.style.display = 'none';
|
212
|
-
});
|
213
|
-
|
214
|
-
const filterContainer = document.querySelector('.dbviewer-table-filter-container');
|
215
|
-
if (filterContainer) {
|
216
|
-
filterContainer.style.position = 'relative';
|
217
|
-
filterContainer.appendChild(clearButton);
|
218
|
-
|
219
|
-
searchInput.addEventListener('input', function() {
|
220
|
-
clearButton.style.display = this.value ? 'block' : 'none';
|
221
|
-
});
|
222
|
-
}
|
223
|
-
|
224
|
-
// Restore saved search filter on page load and apply it immediately
|
225
|
-
const savedFilter = localStorage.getItem(STORAGE_KEYS.searchFilter);
|
226
|
-
if (savedFilter) {
|
227
|
-
searchInput.value = savedFilter;
|
228
|
-
// Show clear button immediately when filter is restored
|
229
|
-
clearButton.style.display = 'block';
|
230
|
-
// Apply filter immediately without debouncing to prevent blinking
|
231
|
-
const query = savedFilter.toLowerCase();
|
232
|
-
const tableItems = document.querySelectorAll('#tablesList .list-group-item-action');
|
233
|
-
let visibleCount = 0;
|
234
|
-
|
235
|
-
tableItems.forEach(function(item) {
|
236
|
-
const tableName = (item.getAttribute('title') || item.textContent).trim().toLowerCase();
|
237
|
-
const displayedText = item.textContent.trim().toLowerCase();
|
238
|
-
|
239
|
-
if (tableName.includes(query) || displayedText.includes(query)) {
|
240
|
-
item.classList.remove('d-none');
|
241
|
-
visibleCount++;
|
242
|
-
} else {
|
243
|
-
item.classList.add('d-none');
|
244
|
-
}
|
245
|
-
});
|
246
|
-
|
247
|
-
// Update the tables count immediately
|
248
|
-
const tableCountElement = document.getElementById('table-count');
|
249
|
-
if (tableCountElement) {
|
250
|
-
tableCountElement.textContent = visibleCount;
|
251
|
-
}
|
252
|
-
|
253
|
-
// Handle no results message immediately
|
254
|
-
let noResultsEl = document.getElementById('dbviewer-no-filter-results');
|
255
|
-
if (visibleCount === 0 && query !== '') {
|
256
|
-
if (!noResultsEl) {
|
257
|
-
noResultsEl = document.createElement('div');
|
258
|
-
noResultsEl.id = 'dbviewer-no-filter-results';
|
259
|
-
noResultsEl.className = 'list-group-item text-muted text-center py-3';
|
260
|
-
noResultsEl.innerHTML = '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
|
261
|
-
document.getElementById('tablesList').appendChild(noResultsEl);
|
262
|
-
}
|
263
|
-
noResultsEl.querySelector('.fw-bold').textContent = query;
|
264
|
-
noResultsEl.style.display = 'block';
|
265
|
-
} else if (noResultsEl) {
|
266
|
-
noResultsEl.style.display = 'none';
|
267
|
-
}
|
268
|
-
}
|
269
|
-
|
270
|
-
// Restore saved scroll position on page load
|
271
|
-
if (sidebarContent) {
|
272
|
-
const savedScrollPosition = localStorage.getItem(STORAGE_KEYS.scrollPosition);
|
273
|
-
if (savedScrollPosition) {
|
274
|
-
// Use requestAnimationFrame to ensure DOM is fully rendered
|
275
|
-
requestAnimationFrame(() => {
|
276
|
-
sidebarContent.scrollTop = parseInt(savedScrollPosition, 10);
|
277
|
-
});
|
278
|
-
}
|
279
|
-
|
280
|
-
// Save scroll position on scroll
|
281
|
-
const saveScrollPosition = debounce(function() {
|
282
|
-
localStorage.setItem(STORAGE_KEYS.scrollPosition, sidebarContent.scrollTop);
|
283
|
-
}, 100);
|
284
|
-
|
285
|
-
sidebarContent.addEventListener('scroll', saveScrollPosition);
|
286
|
-
}
|
287
|
-
|
288
|
-
// Set up event listeners for the search input
|
289
|
-
searchInput.addEventListener('input', filterTables);
|
290
|
-
searchInput.addEventListener('keyup', function(e) {
|
291
|
-
filterTables();
|
292
|
-
|
293
|
-
// Add keyboard navigation for the filtered list
|
294
|
-
if (e.key === 'Enter' || e.key === 'ArrowDown') {
|
295
|
-
e.preventDefault();
|
296
|
-
// Focus the first visible table item (not having d-none class)
|
297
|
-
const firstVisibleItem = document.querySelector('#tablesList .list-group-item-action:not(.d-none)');
|
298
|
-
if (firstVisibleItem) {
|
299
|
-
firstVisibleItem.focus();
|
300
|
-
// Make sure the item is visible in the scrollable area
|
301
|
-
firstVisibleItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
302
|
-
}
|
303
|
-
}
|
304
|
-
});
|
305
|
-
searchInput.addEventListener('search', filterTables); // For clearing via the "x" in some browsers
|
306
|
-
}
|
307
|
-
});
|
308
|
-
</script>
|
@@ -83,6 +83,22 @@ module Dbviewer
|
|
83
83
|
|
84
84
|
# Disable timestamps for better compatibility
|
85
85
|
self.record_timestamps = false
|
86
|
+
|
87
|
+
# Make the model read-only to prevent accidental data modifications
|
88
|
+
def readonly?
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
# Disable write operations at the class level
|
93
|
+
class << self
|
94
|
+
def delete_all(*)
|
95
|
+
raise ActiveRecord::ReadOnlyRecord, "#{name} is a read-only model"
|
96
|
+
end
|
97
|
+
|
98
|
+
def update_all(*)
|
99
|
+
raise ActiveRecord::ReadOnlyRecord, "#{name} is a read-only model"
|
100
|
+
end
|
101
|
+
end
|
86
102
|
end)
|
87
103
|
end
|
88
104
|
end
|
data/lib/dbviewer/version.rb
CHANGED
data/lib/dbviewer.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dbviewer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wailan Tirajoh
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-06-
|
11
|
+
date: 2025-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -67,6 +67,7 @@ files:
|
|
67
67
|
- app/assets/javascripts/dbviewer/home.js
|
68
68
|
- app/assets/javascripts/dbviewer/layout.js
|
69
69
|
- app/assets/javascripts/dbviewer/query.js
|
70
|
+
- app/assets/javascripts/dbviewer/sidebar.js
|
70
71
|
- app/assets/javascripts/dbviewer/table.js
|
71
72
|
- app/assets/stylesheets/dbviewer/application.css
|
72
73
|
- app/assets/stylesheets/dbviewer/entity_relationship_diagram.css
|