dbviewer 0.3.1

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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +250 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/dbviewer/application.css +21 -0
  6. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  7. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  8. data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
  9. data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
  10. data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
  11. data/app/controllers/dbviewer/application_controller.rb +21 -0
  12. data/app/controllers/dbviewer/databases_controller.rb +0 -0
  13. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
  14. data/app/controllers/dbviewer/home_controller.rb +10 -0
  15. data/app/controllers/dbviewer/logs_controller.rb +39 -0
  16. data/app/controllers/dbviewer/tables_controller.rb +73 -0
  17. data/app/helpers/dbviewer/application_helper.rb +118 -0
  18. data/app/jobs/dbviewer/application_job.rb +4 -0
  19. data/app/mailers/dbviewer/application_mailer.rb +6 -0
  20. data/app/models/dbviewer/application_record.rb +5 -0
  21. data/app/services/dbviewer/file_storage.rb +0 -0
  22. data/app/services/dbviewer/in_memory_storage.rb +0 -0
  23. data/app/services/dbviewer/query_analyzer.rb +0 -0
  24. data/app/services/dbviewer/query_collection.rb +0 -0
  25. data/app/services/dbviewer/query_logger.rb +0 -0
  26. data/app/services/dbviewer/query_parser.rb +82 -0
  27. data/app/services/dbviewer/query_storage.rb +0 -0
  28. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
  29. data/app/views/dbviewer/home/index.html.erb +237 -0
  30. data/app/views/dbviewer/logs/index.html.erb +614 -0
  31. data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
  32. data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
  33. data/app/views/dbviewer/tables/index.html.erb +128 -0
  34. data/app/views/dbviewer/tables/query.html.erb +600 -0
  35. data/app/views/dbviewer/tables/show.html.erb +271 -0
  36. data/app/views/layouts/dbviewer/application.html.erb +728 -0
  37. data/config/routes.rb +22 -0
  38. data/lib/dbviewer/configuration.rb +79 -0
  39. data/lib/dbviewer/database_manager.rb +450 -0
  40. data/lib/dbviewer/engine.rb +20 -0
  41. data/lib/dbviewer/initializer.rb +23 -0
  42. data/lib/dbviewer/logger.rb +102 -0
  43. data/lib/dbviewer/query_analyzer.rb +109 -0
  44. data/lib/dbviewer/query_collection.rb +41 -0
  45. data/lib/dbviewer/query_parser.rb +82 -0
  46. data/lib/dbviewer/sql_validator.rb +194 -0
  47. data/lib/dbviewer/storage/base.rb +31 -0
  48. data/lib/dbviewer/storage/file_storage.rb +96 -0
  49. data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
  50. data/lib/dbviewer/version.rb +3 -0
  51. data/lib/dbviewer.rb +65 -0
  52. data/lib/tasks/dbviewer_tasks.rake +4 -0
  53. metadata +126 -0
@@ -0,0 +1,564 @@
1
+ <% content_for :title, "Entity Relationship Diagram" %>
2
+
3
+ <% content_for :sidebar do %>
4
+ <%= render 'dbviewer/shared/sidebar' %>
5
+ <% end %>
6
+
7
+ <div class="container-fluid h-100">
8
+ <div class="row h-100">
9
+ <div class="col-md-12 p-0">
10
+ <div class="card h-100">
11
+ <div class="card-header d-flex justify-content-between align-items-center">
12
+ <h5 class="mb-0">
13
+ <i class="bi bi-diagram-3"></i> Entity Relationship Diagram
14
+ </h5>
15
+ <div class="d-flex align-items-center">
16
+ <span id="zoomPercentage" class="me-2">100%</span>
17
+ <button id="zoomIn" class="btn btn-sm btn-outline-secondary me-1">
18
+ <i class="bi bi-zoom-in"></i>
19
+ </button>
20
+ <button id="zoomOut" class="btn btn-sm btn-outline-secondary me-1">
21
+ <i class="bi bi-zoom-out"></i>
22
+ </button>
23
+ <button id="resetView" class="btn btn-sm btn-outline-secondary me-1">
24
+ <i class="bi bi-arrow-counterclockwise"></i> Reset
25
+ </button>
26
+ <div class="dropdown">
27
+ <button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="downloadButton" data-bs-toggle="dropdown" aria-expanded="false">
28
+ <i class="bi bi-download"></i> Download
29
+ </button>
30
+ <ul class="dropdown-menu" aria-labelledby="downloadButton">
31
+ <li><a class="dropdown-item" href="#" id="downloadSvg">SVG Format</a></li>
32
+ <li><a class="dropdown-item" href="#" id="downloadPng">PNG Format</a></li>
33
+ </ul>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ <div class="card-body p-0">
38
+ <div id="erd-container" class="w-100 h-100">
39
+ <div id="erd-loading" class="d-flex justify-content-center align-items-center h-100">
40
+ <div class="text-center">
41
+ <div class="spinner-border text-primary mb-3" role="status">
42
+ <span class="visually-hidden">Loading...</span>
43
+ </div>
44
+ <p>Generating Entity Relationship Diagram...</p>
45
+ </div>
46
+ </div>
47
+ <!-- The ERD will be rendered here -->
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <%# Include mermaid.js for diagram rendering %>
56
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
57
+ <%# Include svg-pan-zoom for better diagram interaction %>
58
+ <script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
59
+
60
+ <script>
61
+ document.addEventListener('DOMContentLoaded', function() {
62
+ // Initialize mermaid
63
+ mermaid.initialize({
64
+ startOnLoad: true,
65
+ theme: 'neutral',
66
+ securityLevel: 'loose',
67
+ er: {
68
+ diagramPadding: 20,
69
+ layoutDirection: 'TB',
70
+ minEntityWidth: 100,
71
+ minEntityHeight: 75,
72
+ entityPadding: 15,
73
+ stroke: 'gray',
74
+ fill: 'honeydew',
75
+ fontSize: 20
76
+ }
77
+ });
78
+
79
+ // ER Diagram download functionality
80
+ let diagramReady = false;
81
+
82
+ // Function to show a temporary downloading indicator
83
+ function showDownloadingIndicator(format) {
84
+ // Create toast element
85
+ const toastEl = document.createElement('div');
86
+ toastEl.className = 'position-fixed bottom-0 end-0 p-3';
87
+ toastEl.style.zIndex = '5000';
88
+ toastEl.innerHTML = `
89
+ <div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
90
+ <div class="toast-header">
91
+ <strong class="me-auto"><i class="bi bi-download"></i> Downloading ERD</strong>
92
+ <small>just now</small>
93
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
94
+ </div>
95
+ <div class="toast-body">
96
+ <div class="d-flex align-items-center">
97
+ <div class="spinner-border spinner-border-sm me-2" role="status">
98
+ <span class="visually-hidden">Loading...</span>
99
+ </div>
100
+ Preparing ${format} file for download...
101
+ </div>
102
+ </div>
103
+ </div>
104
+ `;
105
+
106
+ document.body.appendChild(toastEl);
107
+
108
+ // Automatically remove after a delay
109
+ setTimeout(() => {
110
+ toastEl.remove();
111
+ }, 3000);
112
+ }
113
+
114
+ // Generate the ERD diagram
115
+ const tables = <%= raw @tables.to_json %>;
116
+ const relationships = <%= raw @table_relationships.to_json %>;
117
+
118
+ console.log(tables, relationships)
119
+
120
+ // Create the ER diagram definition in Mermaid syntax
121
+ let mermaidDefinition = 'erDiagram\n';
122
+
123
+ // We'll store table column data here as we fetch it
124
+ const tableColumns = {};
125
+
126
+ // First pass: add all tables with minimal info
127
+ tables.forEach(function(table) {
128
+ const tableName = table.name;
129
+ mermaidDefinition += ` ${tableName} {\n`;
130
+ mermaidDefinition += ` string id\n`;
131
+ mermaidDefinition += ' }\n';
132
+
133
+ // Start loading column data asynchronously
134
+ fetch(`<%= dbviewer.tables_path %>/${tableName}?format=json`, {
135
+ headers: {
136
+ 'Accept': 'application/json',
137
+ 'X-Requested-With': 'XMLHttpRequest'
138
+ }
139
+ })
140
+ .then(response => response.json())
141
+ .then(data => {
142
+ if (data && data.columns) {
143
+ tableColumns[tableName] = data.columns;
144
+ updateDiagramWithColumns();
145
+ }
146
+ })
147
+ .catch(error => {
148
+ console.error(`Error fetching columns for table ${tableName}:`, error);
149
+ });
150
+ });
151
+
152
+ // Track if we're currently updating the diagram
153
+ let isUpdatingDiagram = false;
154
+
155
+ // Function to update the diagram once we have columns
156
+ function updateDiagramWithColumns() {
157
+ // Prevent multiple simultaneous updates
158
+ if (isUpdatingDiagram) return;
159
+
160
+ // Check if we have all the tables loaded
161
+ if (Object.keys(tableColumns).length === tables.length) {
162
+ isUpdatingDiagram = true;
163
+ console.log('Updating diagram with full column data');
164
+
165
+ // Regenerate the diagram with complete column data
166
+ let updatedDefinition = 'erDiagram\n';
167
+
168
+ tables.forEach(function(table) {
169
+ const tableName = table.name;
170
+ updatedDefinition += ` ${tableName} {\n`;
171
+
172
+ const columns = tableColumns[tableName] || [];
173
+ columns.forEach(column => {
174
+ updatedDefinition += ` ${column.type || 'string'} ${column.name}\n`;
175
+ });
176
+
177
+ updatedDefinition += ' }\n';
178
+ });
179
+
180
+ // Add relationships
181
+ if (relationships && relationships.length > 0) {
182
+ relationships.forEach(function(rel) {
183
+ updatedDefinition += ` ${rel.from_table} }|--|| ${rel.to_table} : "${rel.from_column} → ${rel.to_column}"\n`;
184
+ });
185
+ } else {
186
+ updatedDefinition += ' %% No relationships found in the database schema\n';
187
+ }
188
+
189
+ // Create a new diagram element
190
+ const updatedErdDiv = document.createElement('div');
191
+ updatedErdDiv.className = 'mermaid';
192
+ updatedErdDiv.innerHTML = updatedDefinition;
193
+
194
+ // Get the container but don't clear it yet
195
+ const container = document.getElementById('erd-container');
196
+
197
+ // First, clean up any previous zoom instance
198
+ if (panZoomInstance) {
199
+ panZoomInstance.destroy();
200
+ panZoomInstance = null;
201
+ }
202
+
203
+ // Create a temporary container
204
+ const tempContainer = document.createElement('div');
205
+ tempContainer.style.visibility = 'hidden';
206
+ tempContainer.style.position = 'absolute';
207
+ tempContainer.style.width = '100%';
208
+ tempContainer.appendChild(updatedErdDiv);
209
+ document.body.appendChild(tempContainer);
210
+
211
+ // Render in the temporary container first
212
+ mermaid.init(undefined, updatedErdDiv).then(function() {
213
+ console.log('Diagram fully updated with column data');
214
+
215
+ // Clear original container and move the rendered content
216
+ try {
217
+ // Remove from temp container without destroying
218
+ tempContainer.removeChild(updatedErdDiv);
219
+
220
+ // Clear main container and add the diagram
221
+ container.innerHTML = '';
222
+ container.appendChild(updatedErdDiv);
223
+
224
+ // Remove temp container
225
+ document.body.removeChild(tempContainer);
226
+
227
+ // Wait a bit for the DOM to stabilize before initializing pan-zoom
228
+ setTimeout(() => {
229
+ setupZoomControls();
230
+ // Mark diagram as ready for download
231
+ diagramReady = true;
232
+ isUpdatingDiagram = false;
233
+ }, 100);
234
+ } catch(err) {
235
+ console.error('Error moving diagram to container:', err);
236
+ isUpdatingDiagram = false;
237
+ }
238
+ }).catch(function(error) {
239
+ console.error('Error rendering updated diagram:', error);
240
+ document.body.removeChild(tempContainer);
241
+ isUpdatingDiagram = false;
242
+ });
243
+ }
244
+ }
245
+
246
+ // Add relationships
247
+ if (relationships && relationships.length > 0) {
248
+ relationships.forEach(function(rel) {
249
+ // Format: "Customer ||--o{ Order : places"
250
+ mermaidDefinition += ` ${rel.from_table} }|--|| ${rel.to_table} : "${rel.from_column} → ${rel.to_column}"\n`;
251
+ });
252
+ } else {
253
+ // Add a note if no relationships are found
254
+ mermaidDefinition += ' %% No relationships found in the database schema\n';
255
+ }
256
+
257
+ // Create a div for the initial diagram
258
+ const erdDiv = document.createElement('div');
259
+ erdDiv.className = 'mermaid';
260
+ erdDiv.innerHTML = mermaidDefinition;
261
+
262
+ // Get the container reference for later use
263
+ const container = document.getElementById('erd-container');
264
+
265
+ // Create a temporary container for initial rendering
266
+ const tempInitContainer = document.createElement('div');
267
+ tempInitContainer.style.visibility = 'hidden';
268
+ tempInitContainer.style.position = 'absolute';
269
+ tempInitContainer.style.width = '100%';
270
+ tempInitContainer.appendChild(erdDiv);
271
+ document.body.appendChild(tempInitContainer);
272
+
273
+ // Render the initial diagram in the temporary container
274
+ mermaid.init(undefined, erdDiv).then(function() {
275
+ try {
276
+ // Remove from temp container without destroying
277
+ tempInitContainer.removeChild(erdDiv);
278
+
279
+ // Hide the loading indicator
280
+ document.getElementById('erd-loading').style.display = 'none';
281
+
282
+ // Add the rendered diagram to the main container
283
+ container.appendChild(erdDiv);
284
+
285
+ // Remove temp container
286
+ document.body.removeChild(tempInitContainer);
287
+
288
+ // Setup zoom controls after diagram is rendered
289
+ setTimeout(() => {
290
+ setupZoomControls();
291
+ }, 100);
292
+ } catch(err) {
293
+ console.error('Error moving initial diagram to container:', err);
294
+ }
295
+ }).catch(function(error) {
296
+ console.error('Error rendering diagram:', error);
297
+ document.body.removeChild(tempInitContainer);
298
+ document.getElementById('erd-loading').innerHTML =
299
+ '<div class="alert alert-danger">Error generating diagram. Please try again or check console for details.</div>';
300
+ });
301
+
302
+ // SVG Pan Zoom instance
303
+ let panZoomInstance = null;
304
+
305
+ // Setup zoom controls using svg-pan-zoom library
306
+ function setupZoomControls() {
307
+ const diagramContainer = document.getElementById('erd-container');
308
+ const svgElement = diagramContainer.querySelector('svg');
309
+
310
+ if (!svgElement) {
311
+ console.warn('SVG element not found for zoom controls');
312
+ return;
313
+ }
314
+
315
+ // Make sure SVG has proper attributes for zooming
316
+ svgElement.setAttribute('width', '100%');
317
+ svgElement.setAttribute('height', '100%');
318
+
319
+ // Initialize svg-pan-zoom
320
+ panZoomInstance = svgPanZoom(svgElement, {
321
+ zoomEnabled: true,
322
+ controlIconsEnabled: false,
323
+ fit: true,
324
+ center: true,
325
+ minZoom: 0.1,
326
+ maxZoom: 20,
327
+ zoomScaleSensitivity: 0.3,
328
+ onZoom: function(newZoom) {
329
+ // Update zoom percentage display
330
+ const zoomDisplay = document.getElementById('zoomPercentage');
331
+ if (zoomDisplay) {
332
+ zoomDisplay.textContent = `${Math.round(newZoom * 100)}%`;
333
+ }
334
+ }
335
+ });
336
+
337
+ // Set initial zoom to 100%
338
+ panZoomInstance.zoom(1);
339
+
340
+ // Add event listeners for zoom controls
341
+ document.getElementById('zoomIn').addEventListener('click', function() {
342
+ panZoomInstance.zoomIn();
343
+ });
344
+
345
+ document.getElementById('zoomOut').addEventListener('click', function() {
346
+ panZoomInstance.zoomOut();
347
+ });
348
+
349
+ document.getElementById('resetView').addEventListener('click', function() {
350
+ panZoomInstance.reset();
351
+ });
352
+
353
+ // Update initial percentage display
354
+ const zoomDisplay = document.getElementById('zoomPercentage');
355
+ if (zoomDisplay) {
356
+ zoomDisplay.textContent = '100%';
357
+ }
358
+
359
+ // Mark diagram as ready for download
360
+ diagramReady = true;
361
+ }
362
+
363
+ // Function to download the ERD as SVG
364
+ function downloadAsSVG() {
365
+ if (!diagramReady) {
366
+ alert('Please wait for the diagram to finish loading.');
367
+ return;
368
+ }
369
+
370
+ // Show loading indicator
371
+ showDownloadingIndicator('SVG');
372
+
373
+ try {
374
+ // Get the SVG element
375
+ const svgElement = document.querySelector('#erd-container svg');
376
+ if (!svgElement) {
377
+ alert('SVG diagram not found.');
378
+ return;
379
+ }
380
+
381
+ // Create a clone of the SVG to modify for download
382
+ const clonedSvg = svgElement.cloneNode(true);
383
+
384
+ // Set explicit dimensions to ensure proper rendering
385
+ clonedSvg.setAttribute('width', svgElement.getBoundingClientRect().width);
386
+ clonedSvg.setAttribute('height', svgElement.getBoundingClientRect().height);
387
+
388
+ // Convert SVG to a string
389
+ const serializer = new XMLSerializer();
390
+ let svgString = serializer.serializeToString(clonedSvg);
391
+
392
+ // Add XML declaration and doctype
393
+ svgString = '<?xml version="1.0" standalone="no"?>\n' + svgString;
394
+
395
+ // Create a Blob with the SVG data
396
+ const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
397
+
398
+ // Create a timestamp for filename
399
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
400
+
401
+ // Create download link and trigger download
402
+ const downloadLink = document.createElement('a');
403
+ downloadLink.href = URL.createObjectURL(blob);
404
+ downloadLink.download = `database_erd_${timestamp}.svg`;
405
+ document.body.appendChild(downloadLink);
406
+ downloadLink.click();
407
+ document.body.removeChild(downloadLink);
408
+ } catch (error) {
409
+ console.error('Error downloading SVG:', error);
410
+ alert('Error downloading SVG. Please check console for details.');
411
+ }
412
+ }
413
+
414
+ // Function to download the ERD as PNG
415
+ function downloadAsPNG() {
416
+ if (!diagramReady) {
417
+ alert('Please wait for the diagram to finish loading.');
418
+ return;
419
+ }
420
+
421
+ // Show loading indicator
422
+ showDownloadingIndicator('PNG');
423
+
424
+ try {
425
+ // Get the SVG element
426
+ const svgElement = document.querySelector('#erd-container svg');
427
+ if (!svgElement) {
428
+ alert('SVG diagram not found.');
429
+ return;
430
+ }
431
+
432
+ // Create a clone of the SVG to modify for download
433
+ const clonedSvg = svgElement.cloneNode(true);
434
+
435
+ // Set explicit dimensions to ensure proper rendering
436
+ const width = svgElement.getBoundingClientRect().width;
437
+ const height = svgElement.getBoundingClientRect().height;
438
+ clonedSvg.setAttribute('width', width);
439
+ clonedSvg.setAttribute('height', height);
440
+
441
+ // Convert SVG to a string
442
+ const serializer = new XMLSerializer();
443
+ const svgString = serializer.serializeToString(clonedSvg);
444
+
445
+ // Create a Blob with the SVG data
446
+ const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
447
+ const svgUrl = URL.createObjectURL(svgBlob);
448
+
449
+ // Create an Image object to draw to canvas
450
+ const img = new Image();
451
+ img.onload = function() {
452
+ // Create canvas with appropriate dimensions
453
+ const canvas = document.createElement('canvas');
454
+ canvas.width = width * 2; // Scale up for better quality
455
+ canvas.height = height * 2;
456
+
457
+ // Get drawing context and scale it
458
+ const ctx = canvas.getContext('2d');
459
+ ctx.scale(2, 2); // Scale up for better quality
460
+
461
+ // Draw white background (SVG may have transparency)
462
+ ctx.fillStyle = 'white';
463
+ ctx.fillRect(0, 0, width, height);
464
+
465
+ // Draw the image onto the canvas
466
+ ctx.drawImage(img, 0, 0, width, height);
467
+
468
+ // Create timestamp for filename
469
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
470
+
471
+ // Convert canvas to PNG and trigger download
472
+ canvas.toBlob(function(blob) {
473
+ const downloadLink = document.createElement('a');
474
+ downloadLink.href = URL.createObjectURL(blob);
475
+ downloadLink.download = `database_erd_${timestamp}.png`;
476
+ document.body.appendChild(downloadLink);
477
+ downloadLink.click();
478
+ document.body.removeChild(downloadLink);
479
+ }, 'image/png');
480
+
481
+ // Clean up
482
+ URL.revokeObjectURL(svgUrl);
483
+ };
484
+
485
+ // Set the image source to the SVG URL
486
+ img.src = svgUrl;
487
+ } catch (error) {
488
+ console.error('Error downloading PNG:', error);
489
+ alert('Error downloading PNG. Please check console for details.');
490
+ }
491
+ }
492
+
493
+ // Set up event listeners for download buttons
494
+ document.getElementById('downloadSvg').addEventListener('click', function(e) {
495
+ e.preventDefault();
496
+ downloadAsSVG();
497
+ });
498
+
499
+ document.getElementById('downloadPng').addEventListener('click', function(e) {
500
+ e.preventDefault();
501
+ downloadAsPNG();
502
+ });
503
+ });
504
+ </script>
505
+
506
+ <style>
507
+ #erd-container {
508
+ overflow: auto;
509
+ height: calc(100vh - 125px);
510
+ padding: 20px;
511
+ /* background-color: #fafafa; */
512
+ position: relative;
513
+ }
514
+
515
+ .mermaid {
516
+ display: flex;
517
+ justify-content: center;
518
+ min-width: 100%;
519
+ }
520
+
521
+ /* SVG Pan Zoom styles */
522
+ .svg-pan-zoom_viewport {
523
+ transition: 0.2s;
524
+ }
525
+
526
+ /* Make sure SVG maintains its size */
527
+ #erd-container svg {
528
+ width: 100%;
529
+ height: auto;
530
+ display: block;
531
+ min-width: 800px;
532
+ min-height: 600px;
533
+ }
534
+
535
+ /* Override mermaid defaults for a better look */
536
+ .entityBox {
537
+ fill: #f8f9fa;
538
+ stroke: #6c757d;
539
+ }
540
+
541
+ .entityLabel, .mermaid .label {
542
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
543
+ font-size: 20px !important;
544
+ }
545
+
546
+ /* Zoom percentage display styling */
547
+ #zoomPercentage {
548
+ font-size: 0.9rem;
549
+ /* color: #495057; */
550
+ font-weight: 500;
551
+ width: 45px;
552
+ display: inline-block;
553
+ text-align: center;
554
+ }
555
+
556
+ /* Mermaid override for text size */
557
+ .mermaid .entityLabel div {
558
+ font-size: 20px !important;
559
+ }
560
+
561
+ .mermaid .er.relationshipLabel {
562
+ font-size: 20px !important;
563
+ }
564
+ </style>