dbviewer 0.3.6 → 0.3.15
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/app/controllers/concerns/dbviewer/database_operations.rb +137 -0
- data/app/controllers/dbviewer/tables_controller.rb +34 -0
- data/app/views/dbviewer/home/index.html.erb +37 -16
- data/app/views/dbviewer/tables/mini_erd.html.erb +517 -0
- data/app/views/dbviewer/tables/show.html.erb +566 -21
- data/app/views/layouts/dbviewer/application.html.erb +19 -0
- data/config/routes.rb +1 -0
- data/lib/dbviewer/version.rb +1 -1
- metadata +2 -1
@@ -2,6 +2,37 @@
|
|
2
2
|
Table: <%= @table_name %>
|
3
3
|
<% end %>
|
4
4
|
|
5
|
+
<% content_for :head do %>
|
6
|
+
<!-- Mermaid.js library for ERD diagrams -->
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
8
|
+
<!-- SVG-Pan-Zoom for interactive diagram navigation -->
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
|
10
|
+
<script>
|
11
|
+
// Initialize mermaid when document is ready
|
12
|
+
document.addEventListener('DOMContentLoaded', function() {
|
13
|
+
// Configure Mermaid for better ERD diagrams
|
14
|
+
mermaid.initialize({
|
15
|
+
startOnLoad: false,
|
16
|
+
theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default',
|
17
|
+
securityLevel: 'loose',
|
18
|
+
er: {
|
19
|
+
diagramPadding: 20,
|
20
|
+
layoutDirection: 'TB',
|
21
|
+
minEntityWidth: 100,
|
22
|
+
minEntityHeight: 75,
|
23
|
+
entityPadding: 15,
|
24
|
+
stroke: 'gray',
|
25
|
+
fill: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2D3748' : '#f5f5f5',
|
26
|
+
fontSize: 14,
|
27
|
+
useMaxWidth: true,
|
28
|
+
wrapiength: 30
|
29
|
+
}
|
30
|
+
});
|
31
|
+
console.log('Mermaid initialized with theme:', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default');
|
32
|
+
});
|
33
|
+
</script>
|
34
|
+
<% end %>
|
35
|
+
|
5
36
|
<% content_for :sidebar_active do %>active<% end %>
|
6
37
|
|
7
38
|
<% content_for :sidebar do %>
|
@@ -11,6 +42,9 @@
|
|
11
42
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
12
43
|
<h1>Table: <%= @table_name %></h1>
|
13
44
|
<div class="d-flex gap-2">
|
45
|
+
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#miniErdModal">
|
46
|
+
<i class="bi bi-diagram-3 me-1"></i> View Relationships
|
47
|
+
</button>
|
14
48
|
<% if Dbviewer.configuration.enable_data_export %>
|
15
49
|
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#csvExportModal">
|
16
50
|
<i class="bi bi-file-earmark-spreadsheet me-1"></i> Export CSV
|
@@ -63,6 +97,25 @@
|
|
63
97
|
</div>
|
64
98
|
<% end %>
|
65
99
|
|
100
|
+
<!-- Mini ERD Modal -->
|
101
|
+
<div class="modal fade" id="miniErdModal" tabindex="-1" aria-labelledby="miniErdModalLabel" aria-hidden="true">
|
102
|
+
<div class="modal-dialog modal-xl modal-dialog-centered">
|
103
|
+
<div class="modal-content" id="miniErdModalContent">
|
104
|
+
<!-- Content will be loaded dynamically -->
|
105
|
+
<div class="modal-body text-center p-0">
|
106
|
+
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
|
107
|
+
<div class="text-center">
|
108
|
+
<div class="spinner-border text-primary" role="status">
|
109
|
+
<span class="visually-hidden">Loading...</span>
|
110
|
+
</div>
|
111
|
+
<p class="mt-2">Loading relationships diagram...</p>
|
112
|
+
</div>
|
113
|
+
</div>
|
114
|
+
</div>
|
115
|
+
</div>
|
116
|
+
</div>
|
117
|
+
</div>
|
118
|
+
|
66
119
|
<!-- Records Section -->
|
67
120
|
<div class="dbviewer-card card mb-4">
|
68
121
|
<div class="card-header d-flex justify-content-between align-items-center">
|
@@ -95,44 +148,72 @@
|
|
95
148
|
<table class="table table-bordered table-striped rounded-none">
|
96
149
|
<thead class="dbviewer-table-header">
|
97
150
|
<tr>
|
98
|
-
<% @records.columns
|
99
|
-
|
100
|
-
|
101
|
-
|
151
|
+
<% if @records && @records.columns %>
|
152
|
+
<% @records.columns.each do |column_name| %>
|
153
|
+
<th class="pe-4">
|
154
|
+
<%= column_name %>
|
155
|
+
</th>
|
156
|
+
<% end %>
|
157
|
+
<% else %>
|
158
|
+
<th>No columns available</th>
|
102
159
|
<% end %>
|
103
160
|
</tr>
|
104
161
|
<tr class="column-filters">
|
105
|
-
<% @records.columns
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
162
|
+
<% if @records && @records.columns %>
|
163
|
+
<% @records.columns.each do |column_name| %>
|
164
|
+
<th class="p-0">
|
165
|
+
<%= form.text_field "column_filters[#{column_name}]",
|
166
|
+
value: @column_filters[column_name],
|
167
|
+
placeholder: "",
|
168
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
169
|
+
data: { column: column_name } %>
|
170
|
+
</th>
|
171
|
+
<% end %>
|
172
|
+
<% else %>
|
173
|
+
<th></th>
|
113
174
|
<% end %>
|
114
175
|
</tr>
|
115
176
|
</thead>
|
116
177
|
<tbody>
|
117
|
-
<% if @records.empty? %>
|
178
|
+
<% if @records.nil? || @records.rows.nil? || @records.empty? %>
|
118
179
|
<tr>
|
119
180
|
<td colspan="100%" class="text-center">No records found or table is empty.</td>
|
120
181
|
</tr>
|
121
182
|
<% end %>
|
122
|
-
<% @records.rows
|
123
|
-
|
124
|
-
|
125
|
-
<%
|
126
|
-
|
127
|
-
|
128
|
-
|
183
|
+
<% if @records && @records.rows %>
|
184
|
+
<% @records.rows.each do |row| %>
|
185
|
+
<tr>
|
186
|
+
<% row.each_with_index do |cell, cell_index| %>
|
187
|
+
<%
|
188
|
+
column_name = @records.columns[cell_index]
|
189
|
+
cell_value = format_cell_value(cell)
|
190
|
+
|
191
|
+
# Check if this column is a foreign key
|
192
|
+
foreign_key = @metadata && @metadata[:foreign_keys] ?
|
193
|
+
@metadata[:foreign_keys].find { |fk| fk[:column] == column_name } :
|
194
|
+
nil
|
195
|
+
%>
|
196
|
+
<% if foreign_key && !cell.nil? %>
|
197
|
+
<td title="<%= cell_value %> (Click to view referenced record)">
|
198
|
+
<%= link_to cell_value,
|
199
|
+
table_path(foreign_key[:to_table],
|
200
|
+
column_filters: { foreign_key[:primary_key] => cell }),
|
201
|
+
class: "text-decoration-none foreign-key-link" %>
|
202
|
+
<i class="bi bi-link-45deg text-muted small"></i>
|
203
|
+
</td>
|
204
|
+
<% else %>
|
205
|
+
<td title="<%= cell_value %>"><%= cell_value %></td>
|
206
|
+
<% end %>
|
207
|
+
<% end %>
|
208
|
+
</tr>
|
209
|
+
<% end %>
|
129
210
|
<% end %>
|
130
211
|
</tbody>
|
131
212
|
</table>
|
132
213
|
<% end %> <!-- End of form_with -->
|
133
214
|
</div>
|
134
215
|
|
135
|
-
<% if @total_pages > 1 %>
|
216
|
+
<% if @total_pages && @total_pages > 1 %>
|
136
217
|
<nav aria-label="Page navigation">
|
137
218
|
<ul class="pagination justify-content-center">
|
138
219
|
<li class="page-item <%= 'disabled' if @current_page == 1 %>">
|
@@ -267,6 +348,426 @@
|
|
267
348
|
});
|
268
349
|
}
|
269
350
|
}
|
351
|
+
|
352
|
+
// Load Mini ERD when modal is opened
|
353
|
+
const miniErdModal = document.getElementById('miniErdModal');
|
354
|
+
if (miniErdModal) {
|
355
|
+
let isModalLoaded = false;
|
356
|
+
let erdData = null;
|
357
|
+
|
358
|
+
miniErdModal.addEventListener('show.bs.modal', function(event) {
|
359
|
+
const modalContent = document.getElementById('miniErdModalContent');
|
360
|
+
|
361
|
+
// Set loading state
|
362
|
+
modalContent.innerHTML = `
|
363
|
+
<div class="modal-header">
|
364
|
+
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
365
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
366
|
+
</div>
|
367
|
+
<div class="modal-body p-0">
|
368
|
+
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
|
369
|
+
<div class="text-center">
|
370
|
+
<div class="spinner-border text-primary mb-3" role="status">
|
371
|
+
<span class="visually-hidden">Loading...</span>
|
372
|
+
</div>
|
373
|
+
<p class="mt-2">Loading relationships diagram...</p>
|
374
|
+
<small class="text-muted">This may take a moment for tables with many relationships</small>
|
375
|
+
</div>
|
376
|
+
</div>
|
377
|
+
</div>
|
378
|
+
`;
|
379
|
+
|
380
|
+
// Always fetch fresh data when modal is opened
|
381
|
+
fetchErdData();
|
382
|
+
});
|
383
|
+
|
384
|
+
// Function to fetch ERD data
|
385
|
+
function fetchErdData() {
|
386
|
+
// Add cache-busting timestamp to prevent browser caching
|
387
|
+
const cacheBuster = new Date().getTime();
|
388
|
+
const fetchUrl = `<%= dbviewer.mini_erd_table_path(@table_name, format: :json) %>?_=${cacheBuster}`;
|
389
|
+
|
390
|
+
// Log loading message
|
391
|
+
console.log('Loading fresh Mini ERD data from:', fetchUrl);
|
392
|
+
|
393
|
+
// Set a timeout to handle long-running requests
|
394
|
+
const timeoutPromise = new Promise((_, reject) =>
|
395
|
+
setTimeout(() => reject(new Error('Request timeout after 10 seconds')), 10000)
|
396
|
+
);
|
397
|
+
|
398
|
+
// Race the fetch against a timeout
|
399
|
+
Promise.race([
|
400
|
+
fetch(fetchUrl),
|
401
|
+
timeoutPromise
|
402
|
+
])
|
403
|
+
.then(response => {
|
404
|
+
if (!response.ok) {
|
405
|
+
throw new Error(`Server returned ${response.status} ${response.statusText}`);
|
406
|
+
}
|
407
|
+
return response.json(); // Parse as JSON instead of text
|
408
|
+
})
|
409
|
+
.then(data => {
|
410
|
+
isModalLoaded = true;
|
411
|
+
erdData = data; // Store the data
|
412
|
+
renderMiniErd(data);
|
413
|
+
})
|
414
|
+
.catch(error => {
|
415
|
+
console.error('Error loading mini ERD:', error);
|
416
|
+
showErdError(error);
|
417
|
+
});
|
418
|
+
}
|
419
|
+
|
420
|
+
// Function to show error modal
|
421
|
+
function showErdError(error) {
|
422
|
+
const modalContent = document.getElementById('miniErdModalContent');
|
423
|
+
modalContent.innerHTML = `
|
424
|
+
<div class="modal-header">
|
425
|
+
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
426
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
427
|
+
</div>
|
428
|
+
<div class="modal-body p-0">
|
429
|
+
<div class="alert alert-danger m-3">
|
430
|
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
431
|
+
<strong>Error loading relationship diagram</strong>
|
432
|
+
<p class="mt-2 mb-0">${error.message}</p>
|
433
|
+
</div>
|
434
|
+
<div class="m-3">
|
435
|
+
<p><strong>Debug Information:</strong></p>
|
436
|
+
<code>GET <%= dbviewer.mini_erd_table_path(@table_name, format: :json) %></code> failed
|
437
|
+
<p class="mt-3">
|
438
|
+
<button class="btn btn-sm btn-primary" onclick="retryLoadingMiniERD()">
|
439
|
+
<i class="bi bi-arrow-clockwise me-1"></i> Retry
|
440
|
+
</button>
|
441
|
+
</p>
|
442
|
+
</div>
|
443
|
+
</div>
|
444
|
+
<div class="modal-footer">
|
445
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
446
|
+
</div>
|
447
|
+
`;
|
448
|
+
}
|
449
|
+
|
450
|
+
// Function to render the ERD with Mermaid
|
451
|
+
function renderMiniErd(data) {
|
452
|
+
const modalContent = document.getElementById('miniErdModalContent');
|
453
|
+
|
454
|
+
// Set up the modal content with container for ERD
|
455
|
+
modalContent.innerHTML = `
|
456
|
+
<div class="modal-header">
|
457
|
+
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
458
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
459
|
+
</div>
|
460
|
+
<div class="modal-body p-0"> <!-- Removed padding for full width -->
|
461
|
+
<div id="mini-erd-container" class="w-100" style="min-height: 450px; height: 100%;"> <!-- Increased height -->
|
462
|
+
<div id="mini-erd-loading" class="d-flex justify-content-center align-items-center" style="height: 100%; min-height: 450px;">
|
463
|
+
<div class="text-center">
|
464
|
+
<div class="spinner-border text-primary mb-3" role="status">
|
465
|
+
<span class="visually-hidden">Loading...</span>
|
466
|
+
</div>
|
467
|
+
<p>Generating Relationships Diagram...</p>
|
468
|
+
</div>
|
469
|
+
</div>
|
470
|
+
<div id="mini-erd-error" class="alert alert-danger m-3 d-none">
|
471
|
+
<h5>Error generating diagram</h5>
|
472
|
+
<p id="mini-erd-error-message">There was an error rendering the relationships diagram.</p>
|
473
|
+
<pre id="mini-erd-error-details" class="bg-light p-2 small mt-2"></pre>
|
474
|
+
</div>
|
475
|
+
</div>
|
476
|
+
<div id="debug-data" class="d-none m-3 border-top pt-3">
|
477
|
+
<details>
|
478
|
+
<summary>Debug Information</summary>
|
479
|
+
<div class="alert alert-info small">
|
480
|
+
<pre id="erd-data-debug" style="max-height: 100px; overflow: auto;">${JSON.stringify(data, null, 2)}</pre>
|
481
|
+
</div>
|
482
|
+
</details>
|
483
|
+
</div>
|
484
|
+
</div>
|
485
|
+
<div class="modal-footer">
|
486
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
487
|
+
<a href="<%= dbviewer.entity_relationship_diagrams_path %>" class="btn btn-primary">View Full ERD</a>
|
488
|
+
</div>
|
489
|
+
`;
|
490
|
+
|
491
|
+
try {
|
492
|
+
const tables = data.tables || [];
|
493
|
+
const relationships = data.relationships || [];
|
494
|
+
|
495
|
+
// Validate data before proceeding
|
496
|
+
if (!Array.isArray(tables) || !Array.isArray(relationships)) {
|
497
|
+
showDiagramError('Invalid data format', 'The relationship data is not in the expected format.');
|
498
|
+
console.error('Invalid data format received:', data);
|
499
|
+
return;
|
500
|
+
}
|
501
|
+
|
502
|
+
console.log(`Found ${tables.length} tables and ${relationships.length} relationships`);
|
503
|
+
|
504
|
+
// Create the ER diagram definition in Mermaid syntax
|
505
|
+
let mermaidDefinition = 'erDiagram\n';
|
506
|
+
|
507
|
+
// Add tables to the diagram - ensure we have at least one table
|
508
|
+
if (tables.length === 0) {
|
509
|
+
mermaidDefinition += ` <%= @table_name.gsub(/[^\w]/, '_') %> {\n`;
|
510
|
+
mermaidDefinition += ` string id PK\n`;
|
511
|
+
mermaidDefinition += ` }\n`;
|
512
|
+
} else {
|
513
|
+
tables.forEach(function(table) {
|
514
|
+
const tableName = table.name;
|
515
|
+
|
516
|
+
if (!tableName) {
|
517
|
+
console.warn('Table with no name found:', table);
|
518
|
+
return; // Skip this table
|
519
|
+
}
|
520
|
+
|
521
|
+
// Clean table name for mermaid (remove special characters)
|
522
|
+
const cleanTableName = tableName.replace(/[^\w]/g, '_');
|
523
|
+
|
524
|
+
// Make the current table stand out with a different visualization
|
525
|
+
if (tableName === '<%= @table_name %>') {
|
526
|
+
mermaidDefinition += ` ${cleanTableName} {\n`;
|
527
|
+
mermaidDefinition += ` string id PK\n`;
|
528
|
+
mermaidDefinition += ` }\n`;
|
529
|
+
} else {
|
530
|
+
mermaidDefinition += ` ${cleanTableName} {\n`;
|
531
|
+
mermaidDefinition += ` string id\n`;
|
532
|
+
mermaidDefinition += ` }\n`;
|
533
|
+
}
|
534
|
+
});
|
535
|
+
}
|
536
|
+
|
537
|
+
// Add relationships
|
538
|
+
if (relationships && relationships.length > 0) {
|
539
|
+
relationships.forEach(function(rel) {
|
540
|
+
try {
|
541
|
+
// Ensure all required properties exist
|
542
|
+
if (!rel.from_table || !rel.to_table) {
|
543
|
+
console.error('Missing table in relationship:', rel);
|
544
|
+
return; // Skip this relationship
|
545
|
+
}
|
546
|
+
|
547
|
+
// Clean up table names for mermaid (remove special characters)
|
548
|
+
const fromTable = rel.from_table.replace(/[^\w]/g, '_');
|
549
|
+
const toTable = rel.to_table.replace(/[^\w]/g, '_');
|
550
|
+
const relationLabel = rel.from_column || '';
|
551
|
+
|
552
|
+
// Customize the display based on direction
|
553
|
+
mermaidDefinition += ` ${fromTable} }|--|| ${toTable} : "${relationLabel}"\n`;
|
554
|
+
} catch (err) {
|
555
|
+
console.error('Error processing relationship:', err, rel);
|
556
|
+
}
|
557
|
+
});
|
558
|
+
} else {
|
559
|
+
// Add a note if no relationships are found
|
560
|
+
mermaidDefinition += ' %% No relationships found for this table\n';
|
561
|
+
}
|
562
|
+
|
563
|
+
// Log the generated mermaid definition for debugging
|
564
|
+
console.log('Mermaid Definition:', mermaidDefinition);
|
565
|
+
|
566
|
+
// Hide the loading indicator first since render might take time
|
567
|
+
document.getElementById('mini-erd-loading').style.display = 'none';
|
568
|
+
|
569
|
+
// Render the diagram with Mermaid
|
570
|
+
mermaid.render('mini-erd-graph', mermaidDefinition)
|
571
|
+
.then(function(result) {
|
572
|
+
console.log('Mermaid rendering successful');
|
573
|
+
|
574
|
+
// Get the container
|
575
|
+
const container = document.getElementById('mini-erd-container');
|
576
|
+
|
577
|
+
// Insert the rendered SVG
|
578
|
+
container.innerHTML = result.svg;
|
579
|
+
|
580
|
+
// Style the SVG element for better fit
|
581
|
+
const svgElement = container.querySelector('svg');
|
582
|
+
if (svgElement) {
|
583
|
+
// Set size attributes for the SVG
|
584
|
+
svgElement.setAttribute('width', '100%');
|
585
|
+
svgElement.setAttribute('height', '100%');
|
586
|
+
svgElement.style.minHeight = '450px';
|
587
|
+
svgElement.style.width = '100%';
|
588
|
+
svgElement.style.height = '100%';
|
589
|
+
|
590
|
+
// Set viewBox if not present to enable proper scaling
|
591
|
+
if (!svgElement.getAttribute('viewBox')) {
|
592
|
+
const width = svgElement.getAttribute('width') || '100%';
|
593
|
+
const height = svgElement.getAttribute('height') || '100%';
|
594
|
+
svgElement.setAttribute('viewBox', `0 0 ${parseInt(width) || 1000} ${parseInt(height) || 800}`);
|
595
|
+
}
|
596
|
+
}
|
597
|
+
|
598
|
+
// Apply SVG-Pan-Zoom to make the diagram interactive
|
599
|
+
try {
|
600
|
+
const svgElement = container.querySelector('svg');
|
601
|
+
if (svgElement && typeof svgPanZoom !== 'undefined') {
|
602
|
+
// Make SVG take the full container width
|
603
|
+
svgElement.setAttribute('width', '100%');
|
604
|
+
svgElement.setAttribute('height', '100%');
|
605
|
+
|
606
|
+
// Initialize SVG Pan-Zoom
|
607
|
+
const panZoomInstance = svgPanZoom(svgElement, {
|
608
|
+
zoomEnabled: true,
|
609
|
+
controlIconsEnabled: true,
|
610
|
+
fit: true,
|
611
|
+
center: true,
|
612
|
+
minZoom: 0.5,
|
613
|
+
maxZoom: 2.5
|
614
|
+
});
|
615
|
+
|
616
|
+
// Store the panZoom instance for resize handling
|
617
|
+
container.panZoomInstance = panZoomInstance;
|
618
|
+
|
619
|
+
// Setup resize observer to maintain full size
|
620
|
+
const resizeObserver = new ResizeObserver(() => {
|
621
|
+
if (container.panZoomInstance) {
|
622
|
+
// Reset zoom and center when container is resized
|
623
|
+
container.panZoomInstance.resize();
|
624
|
+
container.panZoomInstance.fit();
|
625
|
+
container.panZoomInstance.center();
|
626
|
+
}
|
627
|
+
});
|
628
|
+
|
629
|
+
// Observe the container for size changes
|
630
|
+
resizeObserver.observe(container);
|
631
|
+
|
632
|
+
// Also handle manual resize on modal resize
|
633
|
+
miniErdModal.addEventListener('resize.bs.modal', function() {
|
634
|
+
if (container.panZoomInstance) {
|
635
|
+
setTimeout(() => {
|
636
|
+
container.panZoomInstance.resize();
|
637
|
+
container.panZoomInstance.fit();
|
638
|
+
container.panZoomInstance.center();
|
639
|
+
}, 100);
|
640
|
+
}
|
641
|
+
});
|
642
|
+
}
|
643
|
+
} catch (e) {
|
644
|
+
console.warn('Failed to initialize svg-pan-zoom:', e);
|
645
|
+
// Not critical, continue without pan-zoom
|
646
|
+
}
|
647
|
+
|
648
|
+
// Add highlighting for the current table
|
649
|
+
setTimeout(function() {
|
650
|
+
try {
|
651
|
+
const cleanTableName = '<%= @table_name %>'.replace(/[^\w]/g, '_');
|
652
|
+
const currentTableElement = container.querySelector(`[id*="${cleanTableName}"]`);
|
653
|
+
if (currentTableElement) {
|
654
|
+
const rect = currentTableElement.querySelector('rect');
|
655
|
+
if (rect) {
|
656
|
+
// Highlight the current table
|
657
|
+
rect.setAttribute('fill', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2c3034' : '#e2f0ff');
|
658
|
+
rect.setAttribute('stroke', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#6ea8fe' : '#0d6efd');
|
659
|
+
rect.setAttribute('stroke-width', '2');
|
660
|
+
}
|
661
|
+
}
|
662
|
+
} catch (e) {
|
663
|
+
console.error('Error highlighting current table:', e);
|
664
|
+
}
|
665
|
+
}, 100);
|
666
|
+
})
|
667
|
+
.catch(function(error) {
|
668
|
+
console.error('Error rendering mini ERD:', error);
|
669
|
+
showDiagramError(
|
670
|
+
'Error rendering diagram',
|
671
|
+
'There was an error rendering the relationships diagram.',
|
672
|
+
error.message || 'Unknown error'
|
673
|
+
);
|
674
|
+
|
675
|
+
// Show debug data when there's an error
|
676
|
+
document.getElementById('debug-data').classList.remove('d-none');
|
677
|
+
});
|
678
|
+
} catch (error) {
|
679
|
+
console.error('Exception in renderMiniErd function:', error);
|
680
|
+
showDiagramError(
|
681
|
+
'Exception generating diagram',
|
682
|
+
'There was an exception processing the relationships diagram.',
|
683
|
+
error.message || 'Unknown error'
|
684
|
+
);
|
685
|
+
|
686
|
+
// Show debug data when there's an error
|
687
|
+
document.getElementById('debug-data').classList.remove('d-none');
|
688
|
+
}
|
689
|
+
}
|
690
|
+
|
691
|
+
// Function to show diagram error
|
692
|
+
function showDiagramError(title, message, details = '') {
|
693
|
+
const errorContainer = document.getElementById('mini-erd-error');
|
694
|
+
const errorMessage = document.getElementById('mini-erd-error-message');
|
695
|
+
const errorDetails = document.getElementById('mini-erd-error-details');
|
696
|
+
const loadingIndicator = document.getElementById('mini-erd-loading');
|
697
|
+
|
698
|
+
if (loadingIndicator) {
|
699
|
+
loadingIndicator.style.display = 'none';
|
700
|
+
}
|
701
|
+
|
702
|
+
if (errorContainer && errorMessage) {
|
703
|
+
// Set error message
|
704
|
+
errorMessage.textContent = message;
|
705
|
+
|
706
|
+
// Set error details if provided
|
707
|
+
if (details && errorDetails) {
|
708
|
+
errorDetails.textContent = details;
|
709
|
+
errorDetails.classList.remove('d-none');
|
710
|
+
} else if (errorDetails) {
|
711
|
+
errorDetails.classList.add('d-none');
|
712
|
+
}
|
713
|
+
|
714
|
+
// Show the error container
|
715
|
+
errorContainer.classList.remove('d-none');
|
716
|
+
}
|
717
|
+
}
|
718
|
+
|
719
|
+
// Handle modal shown event - adjust size after the modal is fully visible
|
720
|
+
miniErdModal.addEventListener('shown.bs.modal', function(event) {
|
721
|
+
// After modal is fully shown, resize the diagram to fit
|
722
|
+
const container = document.getElementById('mini-erd-container');
|
723
|
+
if (container && container.panZoomInstance) {
|
724
|
+
setTimeout(() => {
|
725
|
+
container.panZoomInstance.resize();
|
726
|
+
container.panZoomInstance.fit();
|
727
|
+
container.panZoomInstance.center();
|
728
|
+
}, 200); // Small delay to ensure modal is fully transitioned
|
729
|
+
}
|
730
|
+
});
|
731
|
+
|
732
|
+
// Handle modal close to reset state for future opens
|
733
|
+
miniErdModal.addEventListener('hidden.bs.modal', function(event) {
|
734
|
+
// Reset flags and cached data to ensure fresh fetch on next open
|
735
|
+
isModalLoaded = false;
|
736
|
+
erdData = null;
|
737
|
+
console.log('Modal closed, diagram data will be refetched on next open');
|
738
|
+
});
|
739
|
+
}
|
740
|
+
|
741
|
+
// Function to retry loading the Mini ERD
|
742
|
+
function retryLoadingMiniERD() {
|
743
|
+
console.log('Retrying loading of mini ERD');
|
744
|
+
const modalContent = document.getElementById('miniErdModalContent');
|
745
|
+
|
746
|
+
// Set loading state again
|
747
|
+
modalContent.innerHTML = `
|
748
|
+
<div class="modal-header">
|
749
|
+
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
750
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
751
|
+
</div>
|
752
|
+
<div class="modal-body p-0">
|
753
|
+
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
|
754
|
+
<div class="text-center">
|
755
|
+
<div class="spinner-border text-primary mb-3" role="status">
|
756
|
+
<span class="visually-hidden">Loading...</span>
|
757
|
+
</div>
|
758
|
+
<p>Retrying to load relationships diagram...</p>
|
759
|
+
</div>
|
760
|
+
</div>
|
761
|
+
</div>
|
762
|
+
`;
|
763
|
+
|
764
|
+
// Reset state to ensure fresh fetch
|
765
|
+
isModalLoaded = false;
|
766
|
+
erdData = null;
|
767
|
+
|
768
|
+
// Retry fetching data
|
769
|
+
fetchErdData();
|
770
|
+
}
|
270
771
|
});
|
271
772
|
</script>
|
272
773
|
|
@@ -293,6 +794,50 @@
|
|
293
794
|
color: rgba(255,255,255,0.9);
|
294
795
|
border-color: rgba(255,255,255,0.15);
|
295
796
|
}
|
797
|
+
|
798
|
+
/* Mini ERD modal styling */
|
799
|
+
#miniErdModal .modal-dialog {
|
800
|
+
max-width: 90%;
|
801
|
+
max-height: 90vh;
|
802
|
+
height: 90vh;
|
803
|
+
}
|
804
|
+
|
805
|
+
#miniErdModal .modal-content {
|
806
|
+
height: 100%;
|
807
|
+
}
|
808
|
+
|
809
|
+
#miniErdModal .modal-body {
|
810
|
+
height: calc(100% - 130px); /* Account for header and footer */
|
811
|
+
overflow: hidden; /* Prevent scrollbars within the modal body */
|
812
|
+
}
|
813
|
+
|
814
|
+
#miniErdModal #mini-erd-container {
|
815
|
+
height: 100%;
|
816
|
+
width: 100%;
|
817
|
+
}
|
818
|
+
|
819
|
+
#miniErdModal #mini-erd-container svg {
|
820
|
+
width: 100%;
|
821
|
+
height: 100%;
|
822
|
+
max-height: unset;
|
823
|
+
}
|
824
|
+
|
825
|
+
/* Foreign key link styling */
|
826
|
+
.foreign-key-link {
|
827
|
+
color: var(--bs-primary);
|
828
|
+
position: relative;
|
829
|
+
}
|
830
|
+
|
831
|
+
.foreign-key-link:hover {
|
832
|
+
text-decoration: underline !important;
|
833
|
+
}
|
834
|
+
|
835
|
+
.foreign-key-link + .bi-link-45deg {
|
836
|
+
font-size: 0.75rem;
|
837
|
+
margin-left: 0.25rem;
|
838
|
+
position: relative;
|
839
|
+
top: -1px;
|
840
|
+
}
|
296
841
|
</style>
|
297
842
|
|
298
843
|
<% if @timestamp_data.present? %>
|
@@ -551,6 +551,25 @@
|
|
551
551
|
.stat-card-bg {
|
552
552
|
background-color: var(--bs-light);
|
553
553
|
}
|
554
|
+
|
555
|
+
/* Metric icon styling */
|
556
|
+
.metric-icon {
|
557
|
+
display: flex;
|
558
|
+
align-items: center;
|
559
|
+
justify-content: center;
|
560
|
+
border-radius: 50%;
|
561
|
+
width: 60px;
|
562
|
+
height: 60px;
|
563
|
+
min-width: 60px;
|
564
|
+
text-align: center;
|
565
|
+
background-color: rgba(var(--bs-primary-rgb), 0.1);
|
566
|
+
color: var(--bs-primary);
|
567
|
+
}
|
568
|
+
|
569
|
+
[data-bs-theme="dark"] .metric-icon {
|
570
|
+
background-color: rgba(13, 110, 253, 0.2);
|
571
|
+
color: #6ea8fe;
|
572
|
+
}
|
554
573
|
</style>
|
555
574
|
</head>
|
556
575
|
<body>
|