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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ae5e2964635ef24eaa04e213e09c6ec284b2361d4b23d648297b92b9c276474
4
- data.tar.gz: f205bbb6df4cf8bbc5b748ae91b9843e8704f26d16d37337747ad8023f5825db
3
+ metadata.gz: 50301b7fa1867ac7e266e3b48eb962e7e456de1e43a664750e8885a9b603f6ca
4
+ data.tar.gz: 0377f20859609a2807bdecc8bc5f9118dae6581b7d4819504900e7e6a371dba3
5
5
  SHA512:
6
- metadata.gz: 5541be4f54077ebd58091153cbeb53464e4f32469e8a10220f823d1528e59a90e6edfeba26e910f4362528e69013e537619a3d6c79938024dbca5be3fee490e3
7
- data.tar.gz: d68cb03d0f721cc96561c809ede600b99f702a69c9607719916a60edb431659e9a6eb41abd6b3330de56a0fe48bf48d9d06d3d88a0d9f1b567378330beca1a9e
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", group: :development
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. The logging system offers two storage backends:
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", group: :development
237
+ gem "dbviewer"
264
238
 
265
239
  # Or specify a version
266
- gem "dbviewer", "~> 0.3.2", group: :development
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
- return fetch(apiPath, {
106
- headers: {
107
- Accept: "application/json",
108
- "X-Requested-With": "XMLHttpRequest",
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
- fetch(`${tablePath}/${tableName}?format=json`, {
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
+ });
@@ -7,11 +7,5 @@ module Dbviewer
7
7
  key: current_connection_key
8
8
  }
9
9
  end
10
-
11
- private
12
-
13
- def set_tables
14
- @tables = fetch_tables(include_record_counts: true)
15
- end
16
10
  end
17
11
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.7.1"
2
+ VERSION = "0.7.3"
3
3
  end
data/lib/dbviewer.rb CHANGED
@@ -21,6 +21,8 @@ require "dbviewer/database/metadata_manager"
21
21
  require "dbviewer/datatable/query_operations"
22
22
  require "dbviewer/datatable/query_params"
23
23
 
24
+ require "propshaft"
25
+
24
26
  module Dbviewer
25
27
  # Main module for the database viewer
26
28
 
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.1
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-10 00:00:00.000000000 Z
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