dbviewer 0.5.2 → 0.5.4
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 +92 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +11 -19
- data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +84 -0
- data/app/controllers/dbviewer/api/queries_controller.rb +1 -1
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +5 -6
- data/app/controllers/dbviewer/logs_controller.rb +1 -1
- data/app/controllers/dbviewer/tables_controller.rb +2 -8
- data/app/helpers/dbviewer/application_helper.rb +1 -1
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +232 -141
- data/app/views/dbviewer/tables/show.html.erb +278 -404
- data/config/routes.rb +7 -0
- data/lib/dbviewer/database/cache_manager.rb +78 -0
- data/lib/dbviewer/database/dynamic_model_factory.rb +62 -0
- data/lib/dbviewer/database/manager.rb +204 -0
- data/lib/dbviewer/database/metadata_manager.rb +129 -0
- data/lib/dbviewer/datatable/query_operations.rb +330 -0
- data/lib/dbviewer/datatable/query_params.rb +41 -0
- data/lib/dbviewer/engine.rb +1 -1
- data/lib/dbviewer/query/analyzer.rb +250 -0
- data/lib/dbviewer/query/collection.rb +39 -0
- data/lib/dbviewer/query/executor.rb +93 -0
- data/lib/dbviewer/query/logger.rb +108 -0
- data/lib/dbviewer/query/parser.rb +56 -0
- data/lib/dbviewer/storage/file_storage.rb +0 -3
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +24 -7
- metadata +14 -14
- data/lib/dbviewer/cache_manager.rb +0 -78
- data/lib/dbviewer/database_manager.rb +0 -249
- data/lib/dbviewer/dynamic_model_factory.rb +0 -60
- data/lib/dbviewer/error_handler.rb +0 -18
- data/lib/dbviewer/logger.rb +0 -77
- data/lib/dbviewer/query_analyzer.rb +0 -239
- data/lib/dbviewer/query_collection.rb +0 -37
- data/lib/dbviewer/query_executor.rb +0 -91
- data/lib/dbviewer/query_parser.rb +0 -53
- data/lib/dbviewer/table_metadata_manager.rb +0 -136
- data/lib/dbviewer/table_query_operations.rb +0 -621
- data/lib/dbviewer/table_query_params.rb +0 -39
@@ -33,12 +33,35 @@
|
|
33
33
|
<div class="card-body p-0">
|
34
34
|
<div id="erd-container" class="w-100 h-100" style="min-height: 450px;">
|
35
35
|
<div id="erd-loading" class="d-flex justify-content-center align-items-center h-100" style="min-height: 450px;">
|
36
|
-
<div class="text-center">
|
37
|
-
<div class="
|
38
|
-
<
|
36
|
+
<div class="text-center" style="width: 100%; max-width: 500px;">
|
37
|
+
<div class="mb-4">
|
38
|
+
<i class="bi bi-diagram-3 text-primary" style="font-size: 3rem;"></i>
|
39
39
|
</div>
|
40
|
-
<
|
41
|
-
<
|
40
|
+
<h5 class="mb-3">Generating Entity Relationship Diagram</h5>
|
41
|
+
<p id="loading-phase" class="mb-3">Initializing...</p>
|
42
|
+
|
43
|
+
<!-- Progress bar for table loading -->
|
44
|
+
<div class="progress mb-3" style="height: 8px;">
|
45
|
+
<div id="table-progress-bar" class="progress-bar bg-primary" role="progressbar"
|
46
|
+
style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
|
50
|
+
<!-- Progress text -->
|
51
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
52
|
+
<small class="text-muted">Table Details</small>
|
53
|
+
<small id="table-progress-text" class="text-muted">0 / 0</small>
|
54
|
+
</div>
|
55
|
+
|
56
|
+
<!-- Relationships loading indicator -->
|
57
|
+
<div id="relationships-status" class="d-flex align-items-center justify-content-center mt-3">
|
58
|
+
<div class="spinner-border spinner-border-sm text-secondary me-2" role="status">
|
59
|
+
<span class="visually-hidden">Loading...</span>
|
60
|
+
</div>
|
61
|
+
<small class="text-muted">Loading relationships...</small>
|
62
|
+
</div>
|
63
|
+
|
64
|
+
<small class="text-muted d-block mt-3">This may take a moment for databases with many tables</small>
|
42
65
|
</div>
|
43
66
|
</div>
|
44
67
|
<!-- The ERD will be rendered here -->
|
@@ -150,9 +173,89 @@
|
|
150
173
|
|
151
174
|
// Generate the ERD diagram
|
152
175
|
const tables = <%= raw @tables.to_json %>;
|
153
|
-
|
154
|
-
|
155
|
-
|
176
|
+
|
177
|
+
console.log('Tables:', tables);
|
178
|
+
|
179
|
+
// Initialize empty relationships - will be loaded asynchronously
|
180
|
+
let relationships = [];
|
181
|
+
let relationshipsLoaded = false;
|
182
|
+
|
183
|
+
// Function to fetch relationships asynchronously
|
184
|
+
function fetchRelationships() {
|
185
|
+
return fetch('<%= dbviewer.relationships_api_entity_relationship_diagrams_path %>', {
|
186
|
+
headers: {
|
187
|
+
'Accept': 'application/json',
|
188
|
+
'X-Requested-With': 'XMLHttpRequest'
|
189
|
+
}
|
190
|
+
})
|
191
|
+
.then(response => {
|
192
|
+
if (!response.ok) {
|
193
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
194
|
+
}
|
195
|
+
return response.json();
|
196
|
+
})
|
197
|
+
.then(data => {
|
198
|
+
console.log('Relationships loaded:', data);
|
199
|
+
relationships = data.relationships || [];
|
200
|
+
relationshipsLoaded = true;
|
201
|
+
updateRelationshipsStatus(true);
|
202
|
+
return relationships;
|
203
|
+
})
|
204
|
+
.catch(error => {
|
205
|
+
console.error('Error fetching relationships:', error);
|
206
|
+
relationshipsLoaded = true; // Mark as loaded even on error to prevent infinite loading
|
207
|
+
updateRelationshipsStatus(true);
|
208
|
+
return [];
|
209
|
+
});
|
210
|
+
}
|
211
|
+
|
212
|
+
// Function to update loading status
|
213
|
+
function updateLoadingStatus(message) {
|
214
|
+
const loadingElement = document.getElementById('erd-loading');
|
215
|
+
const loadingPhase = document.getElementById('loading-phase');
|
216
|
+
if (loadingPhase) {
|
217
|
+
loadingPhase.textContent = message;
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
221
|
+
// Function to update table loading progress
|
222
|
+
function updateTableProgress(loaded, total) {
|
223
|
+
const progressBar = document.getElementById('table-progress-bar');
|
224
|
+
const progressText = document.getElementById('table-progress-text');
|
225
|
+
|
226
|
+
if (progressBar && progressText) {
|
227
|
+
const percentage = total > 0 ? Math.round((loaded / total) * 100) : 0;
|
228
|
+
progressBar.style.width = percentage + '%';
|
229
|
+
progressBar.setAttribute('aria-valuenow', percentage);
|
230
|
+
progressText.textContent = `${loaded} / ${total}`;
|
231
|
+
|
232
|
+
// Update progress bar color based on completion
|
233
|
+
if (percentage === 100) {
|
234
|
+
progressBar.classList.remove('bg-primary');
|
235
|
+
progressBar.classList.add('bg-success');
|
236
|
+
}
|
237
|
+
}
|
238
|
+
}
|
239
|
+
|
240
|
+
// Function to update relationships status
|
241
|
+
function updateRelationshipsStatus(loaded) {
|
242
|
+
const relationshipsStatus = document.getElementById('relationships-status');
|
243
|
+
if (relationshipsStatus) {
|
244
|
+
if (loaded) {
|
245
|
+
relationshipsStatus.innerHTML = `
|
246
|
+
<i class="bi bi-check-circle text-success me-2"></i>
|
247
|
+
<small class="text-success">Relationships loaded</small>
|
248
|
+
`;
|
249
|
+
} else {
|
250
|
+
relationshipsStatus.innerHTML = `
|
251
|
+
<div class="spinner-border spinner-border-sm text-secondary me-2" role="status">
|
252
|
+
<span class="visually-hidden">Loading...</span>
|
253
|
+
</div>
|
254
|
+
<small class="text-muted">Loading relationships...</small>
|
255
|
+
`;
|
256
|
+
}
|
257
|
+
}
|
258
|
+
}
|
156
259
|
|
157
260
|
// Create the ER diagram definition in Mermaid syntax
|
158
261
|
let mermaidDefinition = 'erDiagram\n';
|
@@ -160,7 +263,19 @@
|
|
160
263
|
// We'll store table column data here as we fetch it
|
161
264
|
const tableColumns = {};
|
162
265
|
|
163
|
-
//
|
266
|
+
// Track loading progress
|
267
|
+
let columnsLoadedCount = 0;
|
268
|
+
const totalTables = tables.length;
|
269
|
+
|
270
|
+
// Initialize progress bar
|
271
|
+
updateTableProgress(0, totalTables);
|
272
|
+
updateLoadingStatus('Loading table details...');
|
273
|
+
|
274
|
+
// Start fetching relationships immediately
|
275
|
+
updateRelationshipsStatus(false);
|
276
|
+
const relationshipsPromise = fetchRelationships();
|
277
|
+
|
278
|
+
// First pass: add all tables with minimal info and start loading columns
|
164
279
|
tables.forEach(function(table) {
|
165
280
|
const tableName = table.name;
|
166
281
|
mermaidDefinition += ` ${tableName} {\n`;
|
@@ -178,164 +293,124 @@
|
|
178
293
|
.then(data => {
|
179
294
|
if (data && data.columns) {
|
180
295
|
tableColumns[tableName] = data.columns;
|
181
|
-
|
296
|
+
columnsLoadedCount++;
|
297
|
+
|
298
|
+
// Update progress bar
|
299
|
+
updateTableProgress(columnsLoadedCount, totalTables);
|
300
|
+
|
301
|
+
checkIfReadyToUpdate();
|
182
302
|
}
|
183
303
|
})
|
184
304
|
.catch(error => {
|
185
305
|
console.error(`Error fetching columns for table ${tableName}:`, error);
|
306
|
+
columnsLoadedCount++;
|
307
|
+
updateTableProgress(columnsLoadedCount, totalTables);
|
308
|
+
checkIfReadyToUpdate();
|
186
309
|
});
|
187
310
|
});
|
188
311
|
|
312
|
+
// Function to check if we're ready to update the diagram with full data
|
313
|
+
function checkIfReadyToUpdate() {
|
314
|
+
if (columnsLoadedCount === totalTables && relationshipsLoaded) {
|
315
|
+
updateDiagramWithFullData();
|
316
|
+
}
|
317
|
+
}
|
318
|
+
|
319
|
+
// Wait for relationships to load and check if ready
|
320
|
+
relationshipsPromise.finally(() => {
|
321
|
+
checkIfReadyToUpdate();
|
322
|
+
});
|
323
|
+
|
189
324
|
// Track if we're currently updating the diagram
|
190
325
|
let isUpdatingDiagram = false;
|
191
326
|
|
192
|
-
// Function to update the diagram once we have
|
193
|
-
function
|
327
|
+
// Function to update the diagram once we have all data
|
328
|
+
function updateDiagramWithFullData() {
|
194
329
|
// Prevent multiple simultaneous updates
|
195
330
|
if (isUpdatingDiagram) return;
|
196
331
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
332
|
+
isUpdatingDiagram = true;
|
333
|
+
console.log('Rendering diagram with full column and relationship data');
|
334
|
+
|
335
|
+
updateLoadingStatus('Generating final diagram...');
|
336
|
+
|
337
|
+
// Regenerate the diagram with complete data
|
338
|
+
let updatedDefinition = 'erDiagram\n';
|
339
|
+
|
340
|
+
tables.forEach(function(table) {
|
341
|
+
const tableName = table.name;
|
342
|
+
updatedDefinition += ` ${tableName} {\n`;
|
204
343
|
|
205
|
-
|
206
|
-
|
207
|
-
updatedDefinition += `
|
208
|
-
|
209
|
-
const columns = tableColumns[tableName] || [];
|
210
|
-
columns.forEach(column => {
|
211
|
-
updatedDefinition += ` ${column.type || 'string'} ${column.name}\n`;
|
212
|
-
});
|
213
|
-
|
214
|
-
updatedDefinition += ' }\n';
|
344
|
+
const columns = tableColumns[tableName] || [];
|
345
|
+
columns.forEach(column => {
|
346
|
+
updatedDefinition += ` ${column.type || 'string'} ${column.name}\n`;
|
215
347
|
});
|
216
348
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
updatedDefinition +=
|
224
|
-
}
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
tempContainer.appendChild(updatedErdDiv);
|
246
|
-
document.body.appendChild(tempContainer);
|
349
|
+
updatedDefinition += ' }\n';
|
350
|
+
});
|
351
|
+
|
352
|
+
// Add relationships
|
353
|
+
if (relationships && relationships.length > 0) {
|
354
|
+
relationships.forEach(function(rel) {
|
355
|
+
updatedDefinition += ` ${rel.from_table} }|--|| ${rel.to_table} : "${rel.from_column} → ${rel.to_column}"\n`;
|
356
|
+
});
|
357
|
+
} else {
|
358
|
+
updatedDefinition += ' %% No relationships found in the database schema\n';
|
359
|
+
}
|
360
|
+
|
361
|
+
// Create the diagram element
|
362
|
+
const erdDiv = document.createElement('div');
|
363
|
+
erdDiv.className = 'mermaid';
|
364
|
+
erdDiv.innerHTML = updatedDefinition;
|
365
|
+
|
366
|
+
// Create a temporary container for rendering
|
367
|
+
const tempContainer = document.createElement('div');
|
368
|
+
tempContainer.style.visibility = 'hidden';
|
369
|
+
tempContainer.style.position = 'absolute';
|
370
|
+
tempContainer.style.width = '100%';
|
371
|
+
tempContainer.appendChild(erdDiv);
|
372
|
+
document.body.appendChild(tempContainer);
|
373
|
+
|
374
|
+
// Render the diagram in the temporary container
|
375
|
+
mermaid.init(undefined, erdDiv).then(function() {
|
376
|
+
console.log('Diagram fully rendered with all data');
|
247
377
|
|
248
|
-
|
249
|
-
|
250
|
-
|
378
|
+
try {
|
379
|
+
// Remove from temp container without destroying
|
380
|
+
tempContainer.removeChild(erdDiv);
|
251
381
|
|
252
|
-
//
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
// Remove temp container
|
262
|
-
document.body.removeChild(tempContainer);
|
263
|
-
|
264
|
-
// Wait a bit for the DOM to stabilize before initializing pan-zoom
|
265
|
-
setTimeout(() => {
|
266
|
-
setupZoomControls();
|
267
|
-
// Mark diagram as ready for download
|
268
|
-
diagramReady = true;
|
269
|
-
isUpdatingDiagram = false;
|
270
|
-
}, 100);
|
271
|
-
} catch(err) {
|
272
|
-
console.error('Error moving diagram to container:', err);
|
273
|
-
isUpdatingDiagram = false;
|
274
|
-
}
|
275
|
-
}).catch(function(error) {
|
276
|
-
console.error('Error rendering updated diagram:', error);
|
382
|
+
// Hide loading indicator
|
383
|
+
document.getElementById('erd-loading').style.display = 'none';
|
384
|
+
|
385
|
+
// Clear main container and add the diagram
|
386
|
+
container.innerHTML = '';
|
387
|
+
container.appendChild(erdDiv);
|
388
|
+
|
389
|
+
// Remove temp container
|
277
390
|
document.body.removeChild(tempContainer);
|
391
|
+
|
392
|
+
// Wait a bit for the DOM to stabilize before initializing pan-zoom
|
393
|
+
setTimeout(() => {
|
394
|
+
setupZoomControls();
|
395
|
+
// Mark diagram as ready for download
|
396
|
+
diagramReady = true;
|
397
|
+
isUpdatingDiagram = false;
|
398
|
+
}, 100);
|
399
|
+
} catch(err) {
|
400
|
+
console.error('Error moving diagram to container:', err);
|
278
401
|
isUpdatingDiagram = false;
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
if (relationships && relationships.length > 0) {
|
286
|
-
relationships.forEach(function(rel) {
|
287
|
-
// Format: "Customer ||--o{ Order : places"
|
288
|
-
mermaidDefinition += ` ${rel.from_table} }|--|| ${rel.to_table} : "${rel.from_column} → ${rel.to_column}"\n`;
|
402
|
+
}
|
403
|
+
}).catch(function(error) {
|
404
|
+
console.error('Error rendering diagram:', error);
|
405
|
+
document.body.removeChild(tempContainer);
|
406
|
+
isUpdatingDiagram = false;
|
407
|
+
showError('Error rendering diagram', 'There was an error rendering the entity relationship diagram.', error.message);
|
289
408
|
});
|
290
|
-
} else {
|
291
|
-
// Add a note if no relationships are found
|
292
|
-
mermaidDefinition += ' %% No relationships found in the database schema\n';
|
293
409
|
}
|
294
410
|
|
295
|
-
// Create a div for the initial diagram
|
296
|
-
const erdDiv = document.createElement('div');
|
297
|
-
erdDiv.className = 'mermaid';
|
298
|
-
erdDiv.innerHTML = mermaidDefinition;
|
299
|
-
|
300
411
|
// Get the container reference for later use
|
301
412
|
const container = document.getElementById('erd-container');
|
302
413
|
|
303
|
-
// Create a temporary container for initial rendering
|
304
|
-
const tempInitContainer = document.createElement('div');
|
305
|
-
tempInitContainer.style.visibility = 'hidden';
|
306
|
-
tempInitContainer.style.position = 'absolute';
|
307
|
-
tempInitContainer.style.width = '100%';
|
308
|
-
tempInitContainer.appendChild(erdDiv);
|
309
|
-
document.body.appendChild(tempInitContainer);
|
310
|
-
|
311
|
-
// Render the initial diagram in the temporary container
|
312
|
-
mermaid.init(undefined, erdDiv).then(function() {
|
313
|
-
try {
|
314
|
-
// Remove from temp container without destroying
|
315
|
-
tempInitContainer.removeChild(erdDiv);
|
316
|
-
|
317
|
-
// Hide the loading indicator
|
318
|
-
document.getElementById('erd-loading').style.display = 'none';
|
319
|
-
|
320
|
-
// Add the rendered diagram to the main container
|
321
|
-
container.appendChild(erdDiv);
|
322
|
-
|
323
|
-
// Remove temp container
|
324
|
-
document.body.removeChild(tempInitContainer);
|
325
|
-
|
326
|
-
// Setup zoom controls after diagram is rendered
|
327
|
-
setTimeout(() => {
|
328
|
-
setupZoomControls();
|
329
|
-
}, 100);
|
330
|
-
} catch(err) {
|
331
|
-
console.error('Error moving initial diagram to container:', err);
|
332
|
-
}
|
333
|
-
}).catch(function(error) {
|
334
|
-
console.error('Error rendering diagram:', error);
|
335
|
-
document.body.removeChild(tempInitContainer);
|
336
|
-
showError('Error generating diagram', 'There was an error generating the entity relationship diagram.', error.message);
|
337
|
-
});
|
338
|
-
|
339
414
|
// SVG Pan Zoom instance
|
340
415
|
let panZoomInstance = null;
|
341
416
|
|
@@ -677,6 +752,22 @@
|
|
677
752
|
text-align: center;
|
678
753
|
}
|
679
754
|
|
755
|
+
/* Data loading badge styling */
|
756
|
+
#data-loading-badge {
|
757
|
+
animation: pulse 2s infinite;
|
758
|
+
}
|
759
|
+
|
760
|
+
@keyframes pulse {
|
761
|
+
0% { opacity: 1; }
|
762
|
+
50% { opacity: 0.7; }
|
763
|
+
100% { opacity: 1; }
|
764
|
+
}
|
765
|
+
|
766
|
+
#data-loading-badge .badge {
|
767
|
+
font-size: 0.75rem;
|
768
|
+
padding: 0.4rem 0.6rem;
|
769
|
+
}
|
770
|
+
|
680
771
|
/* Mermaid override for text size */
|
681
772
|
.mermaid .entityLabel div {
|
682
773
|
font-size: 20px !important;
|