dbviewer 0.7.6 → 0.7.7
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 +5 -5
- data/Rakefile +1 -1
- data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +153 -20
- data/app/controllers/dbviewer/api/queries_controller.rb +1 -5
- data/app/controllers/dbviewer/tables_controller.rb +1 -6
- data/app/helpers/dbviewer/application_helper.rb +6 -4
- data/app/helpers/dbviewer/database_helper.rb +5 -39
- data/app/helpers/dbviewer/{filter_helper.rb → datatable_ui_filter_helper.rb} +1 -1
- data/app/helpers/dbviewer/datatable_ui_helper.rb +37 -0
- data/app/helpers/dbviewer/{pagination_helper.rb → datatable_ui_pagination_helper.rb} +1 -1
- data/app/helpers/dbviewer/{sorting_helper.rb → datatable_ui_sorting_helper.rb} +1 -1
- data/app/helpers/dbviewer/{table_rendering_helper.rb → datatable_ui_table_helper.rb} +1 -1
- data/app/helpers/dbviewer/formatting_helper.rb +46 -19
- data/app/views/layouts/dbviewer/application.html.erb +37 -37
- data/app/views/layouts/dbviewer/shared/_sidebar.html.erb +1 -18
- data/lib/dbviewer/query/executor.rb +15 -30
- data/lib/dbviewer/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b8c2130ace09b45819bd1fa2d739fecb8a32a75a1348e7c094cb8ce0d66bd71
|
4
|
+
data.tar.gz: 65ff2d9bf93a401c7b193dca798e8afdfe4bb535d7b0c4ee5ce7729312792d49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fdc6983afc3916e25335e0ce9cb33d9525b16dc7c468d24d4a22794c8fb3509d77527a7bd64d48d2e5fd2637eed1b5cea0a3a92d3011a3486b2869d93ca363f9
|
7
|
+
data.tar.gz: e3a401344d1fcf3fc2d2ecedc0ac283295ef6c5c364fe396095b53d284af42430ab3f49dda08b6d2c78e0b6a0cc808977b558a06b0aa00287df52ec5b4bb2125
|
data/README.md
CHANGED
@@ -180,7 +180,7 @@ Dbviewer.configure do |config|
|
|
180
180
|
end
|
181
181
|
```
|
182
182
|
|
183
|
-
Each connection needs to reference an ActiveRecord class that establishes a database connection.
|
183
|
+
Each connection needs to reference an ActiveRecord class that establishes a database connection.
|
184
184
|
|
185
185
|
## 🪵 Query Logging (Development Only)
|
186
186
|
|
@@ -326,21 +326,21 @@ If you prefer to set up manually:
|
|
326
326
|
bundle install
|
327
327
|
|
328
328
|
# Set up the dummy app database
|
329
|
-
cd
|
329
|
+
cd sample/app
|
330
330
|
bin/rails db:prepare
|
331
331
|
bin/rails db:migrate
|
332
332
|
bin/rails db:seed
|
333
333
|
cd ../..
|
334
334
|
|
335
335
|
# Prepare test environment
|
336
|
-
cd
|
336
|
+
cd sample/app && bin/rails db:test:prepare && cd ../..
|
337
337
|
```
|
338
338
|
|
339
339
|
### Development Commands
|
340
340
|
|
341
341
|
```bash
|
342
342
|
# Start the development server
|
343
|
-
cd
|
343
|
+
cd sample/app && bin/rails server
|
344
344
|
|
345
345
|
# Run tests
|
346
346
|
bundle exec rspec
|
@@ -357,7 +357,7 @@ gem build dbviewer.gemspec
|
|
357
357
|
|
358
358
|
### Testing Your Changes
|
359
359
|
|
360
|
-
1. Start the dummy Rails application: `cd
|
360
|
+
1. Start the dummy Rails application: `cd sample/app && bin/rails server`
|
361
361
|
2. Visit `http://localhost:3000/dbviewer` to test your changes
|
362
362
|
3. The dummy app includes sample data across multiple tables to test various DBViewer features
|
363
363
|
|
data/Rakefile
CHANGED
@@ -9,6 +9,15 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
9
9
|
return;
|
10
10
|
}
|
11
11
|
|
12
|
+
// Helper function to debounce rapid function calls
|
13
|
+
function debounce(func, wait) {
|
14
|
+
let timeout;
|
15
|
+
return function (...args) {
|
16
|
+
clearTimeout(timeout);
|
17
|
+
timeout = setTimeout(() => func.apply(this, args), wait);
|
18
|
+
};
|
19
|
+
}
|
20
|
+
|
12
21
|
// Initialize mermaid with theme detection like mini ERD
|
13
22
|
mermaid.initialize({
|
14
23
|
startOnLoad: true,
|
@@ -102,6 +111,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
102
111
|
// Function to fetch relationships asynchronously
|
103
112
|
async function fetchRelationships() {
|
104
113
|
const apiPath = document.getElementById("relationships_api_path").value;
|
114
|
+
updateRelationshipsStatus(false, "Requesting relationships data...");
|
105
115
|
try {
|
106
116
|
const response = await fetch(apiPath, {
|
107
117
|
headers: {
|
@@ -114,15 +124,19 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
114
124
|
throw new Error(`HTTP error! status: ${response.status}`);
|
115
125
|
}
|
116
126
|
|
127
|
+
updateRelationshipsStatus(false, "Processing relationships data...");
|
117
128
|
const data = await response.json();
|
118
129
|
relationships = data.relationships || [];
|
119
130
|
relationshipsLoaded = true;
|
120
|
-
updateRelationshipsStatus(
|
131
|
+
updateRelationshipsStatus(
|
132
|
+
true,
|
133
|
+
`Loaded ${relationships.length} relationships`
|
134
|
+
);
|
121
135
|
return relationships;
|
122
136
|
} catch (error) {
|
123
137
|
console.error("Error fetching relationships:", error);
|
124
138
|
relationshipsLoaded = true; // Mark as loaded even on error to prevent infinite loading
|
125
|
-
updateRelationshipsStatus(true);
|
139
|
+
updateRelationshipsStatus(true, "Failed to load relationships", true);
|
126
140
|
return [];
|
127
141
|
}
|
128
142
|
}
|
@@ -155,20 +169,31 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
155
169
|
}
|
156
170
|
|
157
171
|
// Function to update relationships status
|
158
|
-
function updateRelationshipsStatus(loaded) {
|
172
|
+
function updateRelationshipsStatus(loaded, message, isError = false) {
|
159
173
|
const relationshipsStatus = document.getElementById("relationships-status");
|
160
174
|
if (relationshipsStatus) {
|
161
|
-
if (loaded) {
|
175
|
+
if (loaded && !isError) {
|
162
176
|
relationshipsStatus.innerHTML = `
|
163
177
|
<i class="bi bi-check-circle text-success me-2"></i>
|
164
|
-
<small class="text-success"
|
178
|
+
<small class="text-success">${
|
179
|
+
message || "Relationships loaded"
|
180
|
+
}</small>
|
181
|
+
`;
|
182
|
+
} else if (loaded && isError) {
|
183
|
+
relationshipsStatus.innerHTML = `
|
184
|
+
<i class="bi bi-exclamation-triangle text-warning me-2"></i>
|
185
|
+
<small class="text-warning">${
|
186
|
+
message || "Error loading relationships"
|
187
|
+
}</small>
|
165
188
|
`;
|
166
189
|
} else {
|
167
190
|
relationshipsStatus.innerHTML = `
|
168
191
|
<div class="spinner-border spinner-border-sm text-secondary me-2" role="status">
|
169
192
|
<span class="visually-hidden">Loading...</span>
|
170
193
|
</div>
|
171
|
-
<small class="text-muted"
|
194
|
+
<small class="text-muted">${
|
195
|
+
message || "Loading relationships..."
|
196
|
+
}</small>
|
172
197
|
`;
|
173
198
|
}
|
174
199
|
}
|
@@ -189,10 +214,23 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
189
214
|
updateLoadingStatus("Loading table details...");
|
190
215
|
|
191
216
|
// Start fetching relationships immediately
|
192
|
-
updateRelationshipsStatus(false);
|
217
|
+
updateRelationshipsStatus(false, "Loading relationships...");
|
193
218
|
const relationshipsPromise = fetchRelationships();
|
194
219
|
const tablePath = document.getElementById("tables_path").value;
|
195
220
|
|
221
|
+
// Function to fetch tables in batches for better performance
|
222
|
+
async function fetchTablesInBatches(tables, batchSize = 5) {
|
223
|
+
const batches = [];
|
224
|
+
for (let i = 0; i < tables.length; i += batchSize) {
|
225
|
+
batches.push(tables.slice(i, i + batchSize));
|
226
|
+
}
|
227
|
+
|
228
|
+
for (const batch of batches) {
|
229
|
+
await Promise.all(batch.map((table) => fetchTableColumns(table.name)));
|
230
|
+
// This creates a visual effect of tables loading in batches
|
231
|
+
}
|
232
|
+
}
|
233
|
+
|
196
234
|
// First pass: add all tables with minimal info and start loading columns
|
197
235
|
// Function to fetch column data for a table
|
198
236
|
async function fetchTableColumns(tableName) {
|
@@ -204,6 +242,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
204
242
|
},
|
205
243
|
});
|
206
244
|
|
245
|
+
if (!response.ok) {
|
246
|
+
throw new Error(
|
247
|
+
`Failed to fetch table ${tableName}: ${response.status}`
|
248
|
+
);
|
249
|
+
}
|
250
|
+
|
207
251
|
const data = await response.json();
|
208
252
|
|
209
253
|
if (data && data.columns) {
|
@@ -217,22 +261,29 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
217
261
|
}
|
218
262
|
} catch (error) {
|
219
263
|
console.error(`Error fetching columns for table ${tableName}:`, error);
|
264
|
+
// Add better error handling
|
265
|
+
showError(
|
266
|
+
"Table Loading Error",
|
267
|
+
`Failed to load columns for table ${tableName}`,
|
268
|
+
error.message
|
269
|
+
);
|
220
270
|
columnsLoadedCount++;
|
221
271
|
updateTableProgress(columnsLoadedCount, totalTables);
|
222
272
|
checkIfReadyToUpdate();
|
223
273
|
}
|
224
274
|
}
|
225
275
|
|
276
|
+
// Generate initial table representation
|
226
277
|
tables.forEach(function (table) {
|
227
278
|
const tableName = table.name;
|
228
279
|
mermaidDefinition += ` ${tableName} {\n`;
|
229
280
|
mermaidDefinition += ` string id\n`;
|
230
281
|
mermaidDefinition += " }\n";
|
231
|
-
|
232
|
-
// Start loading column data asynchronously
|
233
|
-
fetchTableColumns(tableName);
|
234
282
|
});
|
235
283
|
|
284
|
+
// Start loading column data asynchronously in batches
|
285
|
+
fetchTablesInBatches(tables);
|
286
|
+
|
236
287
|
// Function to check if we're ready to update the diagram with full data
|
237
288
|
function checkIfReadyToUpdate() {
|
238
289
|
if (columnsLoadedCount === totalTables && relationshipsLoaded) {
|
@@ -381,18 +432,54 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
381
432
|
panZoomInstance.zoom(1);
|
382
433
|
|
383
434
|
// Add event listeners for zoom controls
|
384
|
-
document.getElementById("zoomIn").addEventListener(
|
385
|
-
|
386
|
-
|
435
|
+
document.getElementById("zoomIn").addEventListener(
|
436
|
+
"click",
|
437
|
+
debounce(function () {
|
438
|
+
panZoomInstance.zoomIn();
|
439
|
+
}, 100)
|
440
|
+
);
|
387
441
|
|
388
|
-
document.getElementById("zoomOut").addEventListener(
|
389
|
-
|
390
|
-
|
442
|
+
document.getElementById("zoomOut").addEventListener(
|
443
|
+
"click",
|
444
|
+
debounce(function () {
|
445
|
+
panZoomInstance.zoomOut();
|
446
|
+
}, 100)
|
447
|
+
);
|
448
|
+
|
449
|
+
document.getElementById("resetView").addEventListener(
|
450
|
+
"click",
|
451
|
+
debounce(function () {
|
452
|
+
panZoomInstance.reset();
|
453
|
+
}, 100)
|
454
|
+
);
|
391
455
|
|
392
|
-
|
393
|
-
|
456
|
+
// Add keyboard shortcuts for zoom controls
|
457
|
+
document.addEventListener("keydown", (e) => {
|
458
|
+
if (e.ctrlKey || e.metaKey) {
|
459
|
+
if (e.key === "+" || e.key === "=") {
|
460
|
+
e.preventDefault();
|
461
|
+
panZoomInstance.zoomIn();
|
462
|
+
} else if (e.key === "-") {
|
463
|
+
e.preventDefault();
|
464
|
+
panZoomInstance.zoomOut();
|
465
|
+
} else if (e.key === "0") {
|
466
|
+
e.preventDefault();
|
467
|
+
panZoomInstance.reset();
|
468
|
+
}
|
469
|
+
}
|
394
470
|
});
|
395
471
|
|
472
|
+
// Improve ARIA attributes
|
473
|
+
document
|
474
|
+
.getElementById("zoomIn")
|
475
|
+
.setAttribute("aria-label", "Zoom in diagram");
|
476
|
+
document
|
477
|
+
.getElementById("zoomOut")
|
478
|
+
.setAttribute("aria-label", "Zoom out diagram");
|
479
|
+
document
|
480
|
+
.getElementById("resetView")
|
481
|
+
.setAttribute("aria-label", "Reset diagram view");
|
482
|
+
|
396
483
|
// Update initial percentage display
|
397
484
|
const zoomDisplay = document.getElementById("zoomPercentage");
|
398
485
|
if (zoomDisplay) {
|
@@ -447,12 +534,18 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
447
534
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
448
535
|
|
449
536
|
// Create download link and trigger download
|
537
|
+
const objectURL = URL.createObjectURL(blob);
|
450
538
|
const downloadLink = document.createElement("a");
|
451
|
-
downloadLink.href =
|
539
|
+
downloadLink.href = objectURL;
|
452
540
|
downloadLink.download = `database_erd_${timestamp}.svg`;
|
453
541
|
document.body.appendChild(downloadLink);
|
454
542
|
downloadLink.click();
|
455
543
|
document.body.removeChild(downloadLink);
|
544
|
+
|
545
|
+
// Clean up object URL
|
546
|
+
setTimeout(() => {
|
547
|
+
URL.revokeObjectURL(objectURL);
|
548
|
+
}, 100);
|
456
549
|
} catch (error) {
|
457
550
|
console.error("Error downloading SVG:", error);
|
458
551
|
alert("Error downloading SVG. Please check console for details.");
|
@@ -520,12 +613,18 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
520
613
|
|
521
614
|
// Convert canvas to PNG and trigger download
|
522
615
|
canvas.toBlob(function (blob) {
|
616
|
+
const objectURL = URL.createObjectURL(blob);
|
523
617
|
const downloadLink = document.createElement("a");
|
524
|
-
downloadLink.href =
|
618
|
+
downloadLink.href = objectURL;
|
525
619
|
downloadLink.download = `database_erd_${timestamp}.png`;
|
526
620
|
document.body.appendChild(downloadLink);
|
527
621
|
downloadLink.click();
|
528
622
|
document.body.removeChild(downloadLink);
|
623
|
+
|
624
|
+
// Clean up object URL
|
625
|
+
setTimeout(() => {
|
626
|
+
URL.revokeObjectURL(objectURL);
|
627
|
+
}, 100);
|
529
628
|
}, "image/png");
|
530
629
|
|
531
630
|
// Clean up
|
@@ -554,4 +653,38 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
554
653
|
e.preventDefault();
|
555
654
|
downloadAsPNG();
|
556
655
|
});
|
656
|
+
|
657
|
+
// Add theme observer to update diagram when theme changes
|
658
|
+
function setupThemeObserver() {
|
659
|
+
const observer = new MutationObserver((mutations) => {
|
660
|
+
mutations.forEach((mutation) => {
|
661
|
+
if (mutation.attributeName === "data-bs-theme") {
|
662
|
+
const newTheme =
|
663
|
+
document.documentElement.getAttribute("data-bs-theme");
|
664
|
+
mermaid.initialize({
|
665
|
+
theme: newTheme === "dark" ? "dark" : "default",
|
666
|
+
// Keep other settings
|
667
|
+
securityLevel: "loose",
|
668
|
+
er: {
|
669
|
+
diagramPadding: 20,
|
670
|
+
layoutDirection: "TB",
|
671
|
+
minEntityWidth: 100,
|
672
|
+
minEntityHeight: 75,
|
673
|
+
entityPadding: 15,
|
674
|
+
stroke: "gray",
|
675
|
+
fill: "honeydew",
|
676
|
+
fontSize: 20,
|
677
|
+
},
|
678
|
+
});
|
679
|
+
// Trigger redraw if diagram is already displayed
|
680
|
+
if (diagramReady) {
|
681
|
+
updateDiagramWithFullData();
|
682
|
+
}
|
683
|
+
}
|
684
|
+
});
|
685
|
+
});
|
686
|
+
observer.observe(document.documentElement, { attributes: true });
|
687
|
+
}
|
688
|
+
|
689
|
+
setupThemeObserver();
|
557
690
|
});
|
@@ -13,16 +13,12 @@ module Dbviewer
|
|
13
13
|
def fetch_recent_queries
|
14
14
|
return [] unless query_logging_enabled?
|
15
15
|
|
16
|
-
Dbviewer::Query::Logger.instance.recent_queries(limit:
|
16
|
+
Dbviewer::Query::Logger.instance.recent_queries(limit: 10)
|
17
17
|
end
|
18
18
|
|
19
19
|
def query_logging_enabled?
|
20
20
|
Dbviewer.configuration.enable_query_logging
|
21
21
|
end
|
22
|
-
|
23
|
-
def queries_limit
|
24
|
-
10
|
25
|
-
end
|
26
22
|
end
|
27
23
|
end
|
28
24
|
end
|
@@ -57,12 +57,7 @@ module Dbviewer
|
|
57
57
|
@tables = fetch_tables # Fetch tables for sidebar
|
58
58
|
|
59
59
|
@query = prepare_query(@table_name, params[:query])
|
60
|
-
@records =
|
61
|
-
execute_query(@query)
|
62
|
-
rescue => e
|
63
|
-
@error = "Error executing query: #{e.message}"
|
64
|
-
nil
|
65
|
-
end
|
60
|
+
@records = execute_query(@query)
|
66
61
|
|
67
62
|
render :query
|
68
63
|
end
|
@@ -2,11 +2,13 @@ module Dbviewer
|
|
2
2
|
module ApplicationHelper
|
3
3
|
# Include all the helper modules organized by logical concerns
|
4
4
|
include DatabaseHelper
|
5
|
-
include FilterHelper
|
6
5
|
include FormattingHelper
|
7
|
-
|
8
|
-
include
|
9
|
-
include
|
6
|
+
|
7
|
+
include DatatableUiHelper
|
8
|
+
include DatatableUiFilterHelper
|
9
|
+
include DatatableUiPaginationHelper
|
10
|
+
include DatatableUiSortingHelper
|
11
|
+
include DatatableUiTableHelper
|
10
12
|
include NavigationHelper
|
11
13
|
include UiHelper
|
12
14
|
end
|
@@ -1,5 +1,10 @@
|
|
1
1
|
module Dbviewer
|
2
2
|
module DatabaseHelper
|
3
|
+
# Helper to access the database manager
|
4
|
+
def get_database_manager
|
5
|
+
@database_manager ||= ::Dbviewer::Database::Manager.new
|
6
|
+
end
|
7
|
+
|
3
8
|
# Check if a table has a created_at column
|
4
9
|
def has_timestamp_column?(table_name)
|
5
10
|
return false unless table_name.present?
|
@@ -9,11 +14,6 @@ module Dbviewer
|
|
9
14
|
columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
|
10
15
|
end
|
11
16
|
|
12
|
-
# Helper to access the database manager
|
13
|
-
def get_database_manager
|
14
|
-
@database_manager ||= ::Dbviewer::Database::Manager.new
|
15
|
-
end
|
16
|
-
|
17
17
|
# Extract column type from columns info
|
18
18
|
def column_type_from_info(column_name, columns)
|
19
19
|
return nil unless columns.present?
|
@@ -21,39 +21,5 @@ module Dbviewer
|
|
21
21
|
column_info = columns.find { |c| c[:name].to_s == column_name.to_s }
|
22
22
|
column_info ? column_info[:type].to_s.downcase : nil
|
23
23
|
end
|
24
|
-
|
25
|
-
# Get appropriate icon for column data type
|
26
|
-
def column_type_icon(column_type)
|
27
|
-
case column_type.to_s.downcase
|
28
|
-
when /int/, /serial/, /number/, /decimal/, /float/, /double/
|
29
|
-
"bi-123"
|
30
|
-
when /char/, /text/, /string/, /uuid/
|
31
|
-
"bi-fonts"
|
32
|
-
when /date/, /time/
|
33
|
-
"bi-calendar"
|
34
|
-
when /bool/
|
35
|
-
"bi-toggle-on"
|
36
|
-
when /json/, /jsonb/
|
37
|
-
"bi-braces"
|
38
|
-
when /array/
|
39
|
-
"bi-list-ol"
|
40
|
-
else
|
41
|
-
"bi-file-earmark"
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
# Determine if the current table should be active in the sidebar
|
46
|
-
def current_table?(table_name)
|
47
|
-
@table_name.present? && @table_name == table_name
|
48
|
-
end
|
49
|
-
|
50
|
-
# Format table name for display - truncate if too long
|
51
|
-
def format_table_name(table_name)
|
52
|
-
if table_name.length > 20
|
53
|
-
"#{table_name.first(17)}..."
|
54
|
-
else
|
55
|
-
table_name
|
56
|
-
end
|
57
|
-
end
|
58
24
|
end
|
59
25
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module DatatableUiHelper
|
3
|
+
# Get appropriate icon for column data type
|
4
|
+
def column_type_icon(column_type)
|
5
|
+
case column_type.to_s.downcase
|
6
|
+
when /int/, /serial/, /number/, /decimal/, /float/, /double/
|
7
|
+
"bi-123"
|
8
|
+
when /char/, /text/, /string/, /uuid/
|
9
|
+
"bi-fonts"
|
10
|
+
when /date/, /time/
|
11
|
+
"bi-calendar"
|
12
|
+
when /bool/
|
13
|
+
"bi-toggle-on"
|
14
|
+
when /json/, /jsonb/
|
15
|
+
"bi-braces"
|
16
|
+
when /array/
|
17
|
+
"bi-list-ol"
|
18
|
+
else
|
19
|
+
"bi-file-earmark"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Format table name for display - truncate if too long
|
24
|
+
def format_table_name(table_name)
|
25
|
+
if table_name.length > 20
|
26
|
+
"#{table_name.first(17)}..."
|
27
|
+
else
|
28
|
+
table_name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Determine if the current table should be active in the sidebar
|
33
|
+
def current_table?(table_name)
|
34
|
+
@table_name.present? && @table_name == table_name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Dbviewer
|
2
|
-
module
|
2
|
+
module DatatableUiTableHelper
|
3
3
|
# Render a complete table header row with sortable columns
|
4
4
|
def render_sortable_header_row(records, order_by, order_direction, table_name, current_page, per_page, column_filters)
|
5
5
|
return content_tag(:tr) { content_tag(:th, "No columns available") } unless records&.columns
|
@@ -2,29 +2,56 @@ module Dbviewer
|
|
2
2
|
module FormattingHelper
|
3
3
|
def format_cell_value(value)
|
4
4
|
return "NULL" if value.nil?
|
5
|
-
return value
|
5
|
+
return format_default_value(value) unless value.is_a?(String)
|
6
6
|
|
7
|
+
format_string_value(value)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def format_string_value(value)
|
7
13
|
case value
|
8
|
-
when
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
when /\A\d{4}-\d{2}-\d{2}\z/
|
16
|
-
# Date
|
17
|
-
value
|
18
|
-
when /\A{.+}\z/, /\A\[.+\]\z/
|
19
|
-
# JSON
|
20
|
-
begin
|
21
|
-
JSON.pretty_generate(JSON.parse(value)).truncate(100)
|
22
|
-
rescue
|
23
|
-
value.to_s.truncate(100)
|
24
|
-
end
|
14
|
+
when ->(v) { datetime_string?(v) }
|
15
|
+
format_datetime_value(value)
|
16
|
+
when ->(v) { date_string?(v) }
|
17
|
+
format_date_value(value)
|
18
|
+
when ->(v) { json_string?(v) }
|
19
|
+
format_json_value(value)
|
25
20
|
else
|
26
|
-
value
|
21
|
+
format_default_value(value)
|
27
22
|
end
|
28
23
|
end
|
24
|
+
|
25
|
+
def datetime_string?(value)
|
26
|
+
value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
27
|
+
end
|
28
|
+
|
29
|
+
def date_string?(value)
|
30
|
+
value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
31
|
+
end
|
32
|
+
|
33
|
+
def json_string?(value)
|
34
|
+
value.match?(/\A{.+}\z/) || value.match?(/\A\[.+\]\z/)
|
35
|
+
end
|
36
|
+
|
37
|
+
def format_datetime_value(value)
|
38
|
+
Time.parse(value).strftime("%Y-%m-%d %H:%M:%S")
|
39
|
+
rescue
|
40
|
+
format_default_value(value)
|
41
|
+
end
|
42
|
+
|
43
|
+
def format_date_value(value)
|
44
|
+
value
|
45
|
+
end
|
46
|
+
|
47
|
+
def format_json_value(value)
|
48
|
+
JSON.pretty_generate(JSON.parse(value)).truncate(100)
|
49
|
+
rescue
|
50
|
+
format_default_value(value)
|
51
|
+
end
|
52
|
+
|
53
|
+
def format_default_value(value)
|
54
|
+
value.to_s.truncate(100)
|
55
|
+
end
|
29
56
|
end
|
30
57
|
end
|
@@ -115,44 +115,44 @@
|
|
115
115
|
</div>
|
116
116
|
<div class="offcanvas-body bg-body-tertiary">
|
117
117
|
<ul class="navbar-nav mb-2 mb-lg-0 fw-medium">
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
<% end %>
|
129
|
-
<li class="nav-item dropdown py-1">
|
130
|
-
<a class="nav-link dropdown-toggle d-flex align-items-center rounded" href="#" id="offcanvasDbDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
131
|
-
<i class="bi bi-database me-2 text-primary"></i> <%= (current_conn = available_connections.find { |c| c[:current] }) ? current_conn[:name] : "Database" %>
|
132
|
-
</a>
|
133
|
-
<ul class="dropdown-menu shadow-sm mt-2" aria-labelledby="offcanvasDbDropdown">
|
134
|
-
<% available_connections.each do |connection| %>
|
135
|
-
<li>
|
136
|
-
<%= button_to connection_path(connection[:key]), method: :post, class: "dropdown-item border-0 w-100 text-start #{'active' if connection[:current]}" do %>
|
137
|
-
<% if connection[:current] %>
|
138
|
-
<i class="bi bi-check2-circle me-2 text-primary"></i>
|
139
|
-
<% else %>
|
140
|
-
<i class="bi bi-circle me-2"></i>
|
141
|
-
<% end %>
|
142
|
-
<%= connection[:name] %>
|
143
|
-
<% end %>
|
144
|
-
</li>
|
118
|
+
<li class="nav-item py-1">
|
119
|
+
<%= link_to raw('<i class="bi bi-table me-2 text-primary"></i> Tables'), dbviewer.tables_path, class: "nav-link rounded #{tables_nav_class}" %>
|
120
|
+
</li>
|
121
|
+
<li class="nav-item py-1">
|
122
|
+
<%= link_to raw('<i class="bi bi-diagram-3 me-2 text-primary"></i> ERD'), dbviewer.entity_relationship_diagrams_path, class: "nav-link rounded #{erd_nav_class}" %>
|
123
|
+
</li>
|
124
|
+
<% if Dbviewer.configuration.enable_query_logging %>
|
125
|
+
<li class="nav-item py-1">
|
126
|
+
<%= link_to raw('<i class="bi bi-journal-code me-2 text-primary"></i> SQL Logs'), dbviewer.logs_path, class: "nav-link rounded #{logs_nav_class}" %>
|
127
|
+
</li>
|
145
128
|
<% end %>
|
146
|
-
<li
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
129
|
+
<li class="nav-item dropdown py-1">
|
130
|
+
<a class="nav-link dropdown-toggle d-flex align-items-center rounded" href="#" id="offcanvasDbDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
131
|
+
<i class="bi bi-database me-2 text-primary"></i> <%= (current_conn = available_connections.find { |c| c[:current] }) ? current_conn[:name] : "Database" %>
|
132
|
+
</a>
|
133
|
+
<ul class="dropdown-menu shadow-sm mt-2" aria-labelledby="offcanvasDbDropdown">
|
134
|
+
<% available_connections.each do |connection| %>
|
135
|
+
<li>
|
136
|
+
<%= button_to connection_path(connection[:key]), method: :post, class: "dropdown-item border-0 w-100 text-start #{'active' if connection[:current]}" do %>
|
137
|
+
<% if connection[:current] %>
|
138
|
+
<i class="bi bi-check2-circle me-2 text-primary"></i>
|
139
|
+
<% else %>
|
140
|
+
<i class="bi bi-circle me-2"></i>
|
141
|
+
<% end %>
|
142
|
+
<%= connection[:name] %>
|
143
|
+
<% end %>
|
144
|
+
</li>
|
145
|
+
<% end %>
|
146
|
+
<li><hr class="dropdown-divider"></li>
|
147
|
+
<li><%= link_to "<i class='bi bi-gear me-2'></i> Manage Connections".html_safe, connections_path, class: "dropdown-item" %></li>
|
148
|
+
</ul>
|
149
|
+
</li>
|
150
|
+
<li class="mt-4 pt-2 border-top">
|
151
|
+
<div class="d-flex align-items-center py-2">
|
152
|
+
<i class="bi bi-tools me-2 text-secondary"></i>
|
153
|
+
<span class="text-secondary"><%= Rails.env %> environment</span>
|
154
|
+
</div>
|
155
|
+
</li>
|
156
156
|
</ul>
|
157
157
|
</div>
|
158
158
|
</div>
|
@@ -18,24 +18,7 @@
|
|
18
18
|
<%= link_to table_path(table[:name], table_url_params), title: table[:name],
|
19
19
|
class: "list-group-item list-group-item-action d-flex align-items-center #{'active' if current_table?(table[:name])}",
|
20
20
|
tabindex: "0",
|
21
|
-
data: { table_name: table[:name] }
|
22
|
-
onkeydown: "
|
23
|
-
if(event.key === 'ArrowDown') {
|
24
|
-
event.preventDefault();
|
25
|
-
let next = this.nextElementSibling;
|
26
|
-
while(next && next.classList.contains('d-none')) {
|
27
|
-
next = next.nextElementSibling;
|
28
|
-
}
|
29
|
-
if(next) next.focus();
|
30
|
-
} else if(event.key === 'ArrowUp') {
|
31
|
-
event.preventDefault();
|
32
|
-
let prev = this.previousElementSibling;
|
33
|
-
while(prev && prev.classList.contains('d-none')) {
|
34
|
-
prev = prev.previousElementSibling;
|
35
|
-
}
|
36
|
-
if(prev) prev.focus();
|
37
|
-
else document.getElementById('tableSearch')?.focus();
|
38
|
-
}" do %>
|
21
|
+
data: { table_name: table[:name] } do %>
|
39
22
|
<div class="text-truncate">
|
40
23
|
<i class="bi bi-table me-2"></i>
|
41
24
|
<%= format_table_name(table[:name]) %>
|
@@ -15,28 +15,7 @@ module Dbviewer
|
|
15
15
|
# @return [ActiveRecord::Result] Result set with columns and rows
|
16
16
|
# @raise [StandardError] If the query is invalid or unsafe
|
17
17
|
def execute_query(sql)
|
18
|
-
|
19
|
-
normalized_sql = ::Dbviewer::Validator::Sql.validate!(sql.to_s)
|
20
|
-
|
21
|
-
# Get max records from configuration
|
22
|
-
max_records = @config.max_records || 10000
|
23
|
-
|
24
|
-
# Add a safety limit if not already present
|
25
|
-
unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
|
26
|
-
normalized_sql = "#{normalized_sql} LIMIT #{max_records}"
|
27
|
-
end
|
28
|
-
|
29
|
-
# Log and execute the query
|
30
|
-
Rails.logger.debug("[DBViewer] Executing SQL query: #{normalized_sql}")
|
31
|
-
start_time = Time.now
|
32
|
-
result = @connection.exec_query(normalized_sql)
|
33
|
-
duration = Time.now - start_time
|
34
|
-
|
35
|
-
Rails.logger.debug("[DBViewer] Query completed in #{duration.round(2)}s, returned #{result.rows.size} rows")
|
36
|
-
result
|
37
|
-
rescue => e
|
38
|
-
Rails.logger.error("[DBViewer] SQL query error: #{e.message} for query: #{sql}")
|
39
|
-
raise e
|
18
|
+
exec_query(normalize_sql(sql))
|
40
19
|
end
|
41
20
|
|
42
21
|
# Execute a SQLite PRAGMA command without adding a LIMIT clause
|
@@ -44,14 +23,20 @@ module Dbviewer
|
|
44
23
|
# @return [ActiveRecord::Result] Result set with the PRAGMA value
|
45
24
|
# @raise [StandardError] If the query is invalid or cannot be executed
|
46
25
|
def execute_sqlite_pragma(pragma)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
26
|
+
exec_query("PRAGMA #{pragma}")
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def exec_query(sql)
|
32
|
+
@connection.exec_query(sql)
|
33
|
+
end
|
34
|
+
|
35
|
+
def normalize_sql(sql)
|
36
|
+
normalized_sql = ::Dbviewer::Validator::Sql.validate!(sql.to_s)
|
37
|
+
max_records = @config.max_records || 10000
|
38
|
+
normalized_sql = "#{normalized_sql} LIMIT #{max_records}" unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
|
39
|
+
normalized_sql
|
55
40
|
end
|
56
41
|
end
|
57
42
|
end
|
data/lib/dbviewer/version.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.7
|
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-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -99,12 +99,13 @@ files:
|
|
99
99
|
- app/controllers/dbviewer/tables_controller.rb
|
100
100
|
- app/helpers/dbviewer/application_helper.rb
|
101
101
|
- app/helpers/dbviewer/database_helper.rb
|
102
|
-
- app/helpers/dbviewer/
|
102
|
+
- app/helpers/dbviewer/datatable_ui_filter_helper.rb
|
103
|
+
- app/helpers/dbviewer/datatable_ui_helper.rb
|
104
|
+
- app/helpers/dbviewer/datatable_ui_pagination_helper.rb
|
105
|
+
- app/helpers/dbviewer/datatable_ui_sorting_helper.rb
|
106
|
+
- app/helpers/dbviewer/datatable_ui_table_helper.rb
|
103
107
|
- app/helpers/dbviewer/formatting_helper.rb
|
104
108
|
- app/helpers/dbviewer/navigation_helper.rb
|
105
|
-
- app/helpers/dbviewer/pagination_helper.rb
|
106
|
-
- app/helpers/dbviewer/sorting_helper.rb
|
107
|
-
- app/helpers/dbviewer/table_rendering_helper.rb
|
108
109
|
- app/helpers/dbviewer/ui_helper.rb
|
109
110
|
- app/jobs/dbviewer/application_job.rb
|
110
111
|
- app/mailers/dbviewer/application_mailer.rb
|