dbviewer 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61c23f612495654e7fba0280fd361cf872d668ea21618c74c95d05f4ea7fdb6a
4
- data.tar.gz: eeea9728edb2716f44e66eb1851767a7e77cf38c53f3ee4c676fd3d1d9e3608d
3
+ metadata.gz: 628884438be23468b7910e2a13c14b2aa00cc6361f01cb09b53ecdcf1d8a0b78
4
+ data.tar.gz: e9952d0c11ae3669b2604ac208f1a4ae224c37eb4d5f2b1074c4f284bb0bc24a
5
5
  SHA512:
6
- metadata.gz: 6674c2063fb577a3468ded6ffab856bcc8092f5a6fa46a28eb727f3e6a140556401c05777a7bc4ab7dd9e2a78c90b4576800db5a6b724f31d6a1e8f649cc1392
7
- data.tar.gz: 1714aa447da6c73754b4702e2114e63dca6c1dce5be4be790bd0b7ae89aa7972f9bbbfc54de588da5237f45ca69066f2fde1b50ae15017e0a52842c954ee9372
6
+ metadata.gz: 369f106d728156712416e6d1f65c51e156ec1357e717c5cc4f95e9fe16dbe696e3a06b8c70a2ef2fd6ae559dd0e7ad7b73fc6a5b2e0ccc396a756633d733c2dd
7
+ data.tar.gz: 273df74ea6e69b327b01faecb4b3572983fbd461ee042c41ba2402729482ead2b9ebaed117c95158ff01def2e1a1373aaf0fed75d8268d8546b0f3e38d2ff2ae
data/README.md CHANGED
@@ -5,7 +5,8 @@
5
5
  DBViewer is a powerful Rails engine that provides a comprehensive interface to view and explore database tables, records, and schema.
6
6
  It's designed for development, debugging, and database analysis, offering a clean and intuitive way to interact with your application's database.
7
7
 
8
- <img width="1470" alt="image" src="https://github.com/user-attachments/assets/c946a286-e80a-4cca-afa0-654052e4ef2c" />
8
+ <img width="1470" alt="image" src="https://github.com/user-attachments/assets/0d2719ad-f5b4-4818-891d-5bff7be6c5c3" />
9
+
9
10
 
10
11
  ## ✨ Features
11
12
 
@@ -45,27 +46,10 @@ It's designed for development, debugging, and database analysis, offering a clea
45
46
 
46
47
  <details>
47
48
  <summary>Click to see more screenshots</summary>
48
-
49
- #### Dashboard Overview
50
-
51
- <img width="1470" alt="image" src="https://github.com/user-attachments/assets/4e803d51-9a5b-4c80-bb4c-a761dba15a40" />
52
-
53
- #### Table Details
54
-
55
- <img width="1470" alt="image" src="https://github.com/user-attachments/assets/fe425ab4-5b22-4839-87bc-050b80ad4cf0" />
56
-
57
- #### Query Editor
58
-
59
- <img width="1470" alt="image" src="https://github.com/user-attachments/assets/392c73c7-0724-4a39-8ffa-8ff5115c5d5f" />
60
-
61
- #### Query Logs
62
-
63
- <img width="1470" alt="image" src="https://github.com/user-attachments/assets/7fcf3355-be3c-4d6a-9ab0-811333be5bbc" />
64
49
 
65
- #### ERD
50
+ <img width="1470" alt="image" src="https://github.com/user-attachments/assets/7d708c14-5f78-42c4-b769-2167546b3aad" />
51
+ <img width="1470" alt="image" src="https://github.com/user-attachments/assets/f6d9a39a-a571-4328-908a-d96b3148f707" />
66
52
 
67
- <img width="1470" alt="image" src="https://github.com/user-attachments/assets/0a2f838f-4ca6-4592-b939-7c7f8ac40f48" />
68
-
69
53
  </details>
70
54
 
71
55
  ## 📥 Installation
@@ -50,7 +50,6 @@ module Dbviewer
50
50
  database_manager.tables.map do |table_name|
51
51
  table_stats = {
52
52
  name: table_name
53
- # columns_count: database_manager.column_count(table_name)
54
53
  }
55
54
 
56
55
  # Only fetch record counts if explicitly requested
@@ -72,36 +71,8 @@ module Dbviewer
72
71
  largest_tables: tables.sort_by { |t| -t[:record_count] }.first(10),
73
72
  empty_tables: tables.select { |t| t[:record_count] == 0 }
74
73
  }
75
-
76
- # Calculate total foreign key relationships
77
- begin
78
- total_relationships = 0
79
- tables.each do |table|
80
- metadata = fetch_table_metadata(table[:name])
81
- total_relationships += metadata[:foreign_keys].size if metadata && metadata[:foreign_keys]
82
- end
83
- analytics[:total_relationships] = total_relationships
84
- rescue => e
85
- Rails.logger.error("Error calculating relationship count: #{e.message}")
86
- analytics[:total_relationships] = 0
87
- end
88
-
89
74
  # Calculate schema size if possible
90
- begin
91
- analytics[:schema_size] = calculate_schema_size
92
- rescue => e
93
- Rails.logger.error("Error calculating schema size: #{e.message}")
94
- analytics[:schema_size] = nil
95
- end
96
-
97
- # Calculate average rows per table
98
- if tables.any?
99
- analytics[:avg_records_per_table] = (analytics[:total_records].to_f / tables.size).round(1)
100
- analytics[:avg_columns_per_table] = (analytics[:total_columns].to_f / tables.size).round(1)
101
- else
102
- analytics[:avg_records_per_table] = 0
103
- analytics[:avg_columns_per_table] = 0
104
- end
75
+ analytics[:schema_size] = calculate_schema_size
105
76
 
106
77
  analytics
107
78
  end
@@ -0,0 +1,19 @@
1
+ module Dbviewer
2
+ module Api
3
+ class BaseController < ApplicationController
4
+ # Skip setting the tables instance variable for API endpoints since we don't need it
5
+ skip_before_action :set_tables
6
+
7
+ # Common API response handling for errors
8
+ def render_error(error_message, status = :internal_server_error)
9
+ Rails.logger.error(error_message)
10
+ render json: { error: error_message }, status: status
11
+ end
12
+
13
+ # Common API response handling for success
14
+ def render_success(data)
15
+ render json: data
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Dbviewer
2
+ module Api
3
+ class DatabaseController < BaseController
4
+ def size
5
+ begin
6
+ size = calculate_schema_size
7
+ render_success(schema_size: size)
8
+ rescue => e
9
+ render_error("Error calculating schema size: #{e.message}")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module Dbviewer
2
+ module Api
3
+ class QueriesController < BaseController
4
+ def recent
5
+ @recent_queries = if Dbviewer.configuration.enable_query_logging
6
+ Dbviewer::Logger.instance.recent_queries(limit: 10)
7
+ else
8
+ []
9
+ end
10
+
11
+ render_success({
12
+ enabled: Dbviewer.configuration.enable_query_logging,
13
+ queries: @recent_queries
14
+ })
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ module Dbviewer
2
+ module Api
3
+ class TablesController < BaseController
4
+ def index
5
+ tables = fetch_tables_with_stats(include_record_counts: false)
6
+ render_success(total_tables: tables.size)
7
+ end
8
+
9
+ def records
10
+ tables = fetch_tables_with_stats(include_record_counts: true)
11
+
12
+ records_data = {
13
+ total_records: tables.sum { |t| t[:record_count] },
14
+ largest_tables: tables.sort_by { |t| -t[:record_count] }.first(10),
15
+ empty_tables: tables.select { |t| t[:record_count] == 0 },
16
+ avg_records_per_table: tables.any? ? (tables.sum { |t| t[:record_count] }.to_f / tables.size).round(1) : 0
17
+ }
18
+
19
+ render_success(records_data)
20
+ end
21
+
22
+ def relationships_count
23
+ begin
24
+ tables = fetch_tables_with_stats(include_record_counts: false)
25
+ total_relationships = 0
26
+
27
+ tables.each do |table|
28
+ metadata = fetch_table_metadata(table[:name])
29
+ total_relationships += metadata[:foreign_keys].size if metadata && metadata[:foreign_keys]
30
+ end
31
+
32
+ render_success(total_relationships: total_relationships)
33
+ rescue => e
34
+ render_error("Error calculating relationship count: #{e.message}")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,98 +1,6 @@
1
1
  module Dbviewer
2
2
  class HomeController < ApplicationController
3
3
  def index
4
- # Load page immediately without heavy data
5
- # Data will be loaded asynchronously via AJAX
6
- end
7
-
8
- def analytics
9
- # This method is deprecated but kept for backward compatibility
10
- analytics_data = fetch_database_analytics
11
- # Remove record data which will be served by the records endpoint
12
- analytics_data.delete(:total_records)
13
- analytics_data.delete(:largest_tables)
14
- analytics_data.delete(:empty_tables)
15
- analytics_data.delete(:avg_records_per_table)
16
-
17
- respond_to do |format|
18
- format.json { render json: analytics_data }
19
- end
20
- end
21
-
22
- def tables_count
23
- tables = fetch_tables_with_stats(include_record_counts: false)
24
-
25
- respond_to do |format|
26
- format.json { render json: { total_tables: tables.size } }
27
- end
28
- end
29
-
30
- def relationships_count
31
- begin
32
- tables = fetch_tables_with_stats(include_record_counts: false)
33
- total_relationships = 0
34
-
35
- tables.each do |table|
36
- metadata = fetch_table_metadata(table[:name])
37
- total_relationships += metadata[:foreign_keys].size if metadata && metadata[:foreign_keys]
38
- end
39
-
40
- respond_to do |format|
41
- format.json { render json: { total_relationships: total_relationships } }
42
- end
43
- rescue => e
44
- Rails.logger.error("Error calculating relationship count: #{e.message}")
45
- respond_to do |format|
46
- format.json { render json: { total_relationships: 0, error: e.message }, status: :internal_server_error }
47
- end
48
- end
49
- end
50
-
51
- def database_size
52
- begin
53
- size = calculate_schema_size
54
-
55
- respond_to do |format|
56
- format.json { render json: { schema_size: size } }
57
- end
58
- rescue => e
59
- Rails.logger.error("Error calculating schema size: #{e.message}")
60
- respond_to do |format|
61
- format.json { render json: { schema_size: nil, error: e.message }, status: :internal_server_error }
62
- end
63
- end
64
- end
65
-
66
- def records
67
- tables = fetch_tables_with_stats(include_record_counts: true)
68
-
69
- records_data = {
70
- total_records: tables.sum { |t| t[:record_count] },
71
- largest_tables: tables.sort_by { |t| -t[:record_count] }.first(10),
72
- empty_tables: tables.select { |t| t[:record_count] == 0 },
73
- avg_records_per_table: tables.any? ? (tables.sum { |t| t[:record_count] }.to_f / tables.size).round(1) : 0
74
- }
75
-
76
- respond_to do |format|
77
- format.json { render json: records_data }
78
- end
79
- end
80
-
81
- def recent_queries
82
- @recent_queries = if Dbviewer.configuration.enable_query_logging
83
- Dbviewer::Logger.instance.recent_queries(limit: 10)
84
- else
85
- []
86
- end
87
-
88
- respond_to do |format|
89
- format.json do
90
- render json: {
91
- enabled: Dbviewer.configuration.enable_query_logging,
92
- queries: @recent_queries
93
- }
94
- end
95
- end
96
4
  end
97
5
 
98
6
  private
@@ -103,11 +103,9 @@ module Dbviewer
103
103
  @current_page = [ 1, params[:page].to_i ].max
104
104
  @per_page = params[:per_page] ? params[:per_page].to_i : self.class.default_per_page
105
105
  @per_page = self.class.default_per_page unless self.class.per_page_options.include?(@per_page)
106
- @order_by = params[:order_by].presence ||
107
- database_manager.primary_key(@table_name).presence ||
108
- (@columns.first ? @columns.first[:name] : nil)
106
+ @order_by = params[:order_by].presence || determine_default_order_column
109
107
  @order_direction = params[:order_direction].upcase if params[:order_direction].present?
110
- @order_direction = "ASC" unless self.class::VALID_SORT_DIRECTIONS.include?(@order_direction)
108
+ @order_direction = "DESC" unless self.class::VALID_SORT_DIRECTIONS.include?(@order_direction)
111
109
  @column_filters = params[:column_filters].presence ? params[:column_filters].to_enum.to_h : {}
112
110
  end
113
111
 
@@ -158,5 +156,23 @@ module Dbviewer
158
156
  end
159
157
  end
160
158
  end
159
+
160
+ # Determine the default order column using configurable ordering logic
161
+ def determine_default_order_column
162
+ # Get the table columns to check what's available
163
+ columns = @columns || fetch_table_columns(@table_name)
164
+ column_names = columns.map { |col| col[:name] }
165
+
166
+ # Try the configured default order column first
167
+ default_column = Dbviewer.configuration.default_order_column
168
+ return default_column if default_column && column_names.include?(default_column)
169
+
170
+ # Fall back to primary key
171
+ primary_key = database_manager.primary_key(@table_name)
172
+ return primary_key if primary_key.present?
173
+
174
+ # Final fallback to first column
175
+ columns.first ? columns.first[:name] : nil
176
+ end
161
177
  end
162
178
  end
@@ -8,7 +8,7 @@
8
8
  </div>
9
9
  </div>
10
10
 
11
- <div class="row g-3 mb-4" id="analytics-cards">
11
+ <div class="row g-3 mb-4 dashboard-analytics-cards">
12
12
  <div class="col-md-4">
13
13
  <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
14
14
  <div class="card-body d-flex align-items-center">
@@ -299,7 +299,7 @@ document.addEventListener('DOMContentLoaded', function() {
299
299
  }
300
300
 
301
301
  // Load tables count data
302
- fetch('<%= api_tables_path %>', {
302
+ fetch('<%= dbviewer.api_tables_path %>', {
303
303
  headers: {
304
304
  'Accept': 'application/json',
305
305
  'X-Requested-With': 'XMLHttpRequest'
@@ -324,7 +324,7 @@ document.addEventListener('DOMContentLoaded', function() {
324
324
  });
325
325
 
326
326
  // Load database size data
327
- fetch('<%= api_database_size_path %>', {
327
+ fetch('<%= dbviewer.size_api_database_path %>', {
328
328
  headers: {
329
329
  'Accept': 'application/json',
330
330
  'X-Requested-With': 'XMLHttpRequest'
@@ -349,7 +349,7 @@ document.addEventListener('DOMContentLoaded', function() {
349
349
  });
350
350
 
351
351
  // Load records data separately
352
- fetch('<%= api_records_path %>', {
352
+ fetch('<%= dbviewer.records_api_tables_path %>', {
353
353
  headers: {
354
354
  'Accept': 'application/json',
355
355
  'X-Requested-With': 'XMLHttpRequest'
@@ -377,7 +377,7 @@ document.addEventListener('DOMContentLoaded', function() {
377
377
  });
378
378
 
379
379
  // Load recent queries data
380
- fetch('<%= api_recent_queries_path %>', {
380
+ fetch('<%= dbviewer.recent_api_queries_path %>', {
381
381
  headers: {
382
382
  'Accept': 'application/json',
383
383
  'X-Requested-With': 'XMLHttpRequest'
@@ -400,73 +400,173 @@ document.addEventListener('DOMContentLoaded', function() {
400
400
  </script>
401
401
 
402
402
  <style>
403
+ /* ================================================
404
+ CSS Custom Properties (CSS Variables)
405
+ ================================================ */
406
+ :root {
407
+ /* Colors */
408
+ --dbviewer-code-bg: rgba(0, 0, 0, 0.05);
409
+ --dbviewer-code-border: rgba(0, 0, 0, 0.1);
410
+ --dbviewer-muted-color: #6c757d;
411
+ --dbviewer-success-color: #28a745;
412
+ --dbviewer-danger-color: #dc3545;
413
+ --dbviewer-warning-color: #ffc107;
414
+
415
+ /* Skeleton loader colors */
416
+ --skeleton-base-color: #f0f0f0;
417
+ --skeleton-highlight-color: #e0e0e0;
418
+
419
+ /* Typography */
420
+ --dbviewer-monospace-font: 'Courier New', Courier, monospace;
421
+ --dbviewer-code-font-size: 0.85rem;
422
+
423
+ /* Spacing and sizing */
424
+ --dbviewer-border-radius: 4px;
425
+ --dbviewer-border-radius-sm: 3px;
426
+ --dbviewer-padding-sm: 2px 4px;
427
+ }
428
+
429
+ /* ================================================
430
+ Dark Mode Support
431
+ ================================================ */
432
+ @media (prefers-color-scheme: dark) {
433
+ :root {
434
+ --dbviewer-code-bg: rgba(255, 255, 255, 0.1);
435
+ --dbviewer-code-border: rgba(255, 255, 255, 0.15);
436
+ --dbviewer-muted-color: #adb5bd;
437
+ --skeleton-base-color: #2a2a2a;
438
+ --skeleton-highlight-color: #404040;
439
+ }
440
+ }
441
+
442
+ /* Bootstrap dark mode support */
443
+ [data-bs-theme="dark"] {
444
+ --dbviewer-code-bg: rgba(255, 255, 255, 0.1);
445
+ --dbviewer-code-border: rgba(255, 255, 255, 0.15);
446
+ --dbviewer-muted-color: #adb5bd;
447
+ --skeleton-base-color: #2a2a2a;
448
+ --skeleton-highlight-color: #404040;
449
+ }
450
+
451
+ /* ================================================
452
+ SQL Query Styling
453
+ ================================================ */
403
454
  .sql-query-code {
404
- font-family: 'Courier New', Courier, monospace;
405
- font-size: 0.85rem;
406
- background-color: rgba(0, 0, 0, 0.05);
407
- padding: 2px 4px;
408
- border-radius: 3px;
455
+ font-family: var(--dbviewer-monospace-font);
456
+ font-size: var(--dbviewer-code-font-size);
457
+ background-color: var(--dbviewer-code-bg);
458
+ padding: var(--dbviewer-padding-sm);
459
+ border-radius: var(--dbviewer-border-radius-sm);
460
+ border: 1px solid var(--dbviewer-code-border);
461
+ transition: background-color 0.2s ease, border-color 0.2s ease;
409
462
  }
410
-
463
+
464
+ .sql-query-code:hover {
465
+ background-color: var(--dbviewer-code-bg);
466
+ filter: brightness(0.95);
467
+ }
468
+
469
+ /* ================================================
470
+ Query Performance Indicators
471
+ ================================================ */
411
472
  .query-duration {
412
- color: #28a745;
473
+ color: var(--dbviewer-success-color);
413
474
  font-weight: 500;
475
+ font-variant-numeric: tabular-nums;
476
+ transition: color 0.2s ease;
414
477
  }
415
478
 
416
479
  .query-duration-slow {
417
- color: #dc3545;
480
+ color: var(--dbviewer-danger-color);
418
481
  font-weight: 600;
482
+ font-variant-numeric: tabular-nums;
483
+ transition: color 0.2s ease;
419
484
  }
420
485
 
421
486
  .query-timestamp {
422
- color: #6c757d;
487
+ color: var(--dbviewer-muted-color);
488
+ font-variant-numeric: tabular-nums;
489
+ transition: color 0.2s ease;
423
490
  }
424
-
491
+
492
+ /* ================================================
493
+ Empty States and Messages
494
+ ================================================ */
425
495
  .empty-data-message {
426
- color: #6c757d;
496
+ color: var(--dbviewer-muted-color);
497
+ transition: color 0.2s ease;
427
498
  }
428
-
429
- /* Loading animations */
499
+
500
+ .empty-data-message p {
501
+ margin-bottom: 0.5rem;
502
+ font-weight: 500;
503
+ }
504
+
505
+ .empty-data-message small {
506
+ opacity: 0.8;
507
+ }
508
+
509
+ /* ================================================
510
+ Loading States
511
+ ================================================ */
430
512
  .spinner-border-sm {
431
513
  width: 1rem;
432
514
  height: 1rem;
433
515
  }
434
-
435
- /* Skeleton loader styles */
516
+
517
+ /* ================================================
518
+ Skeleton Loader System
519
+ ================================================ */
436
520
  .skeleton-loader {
437
521
  display: inline-block;
438
522
  height: 1.2em;
439
523
  width: 100%;
440
- background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 37%, #f0f0f0 63%);
524
+ background: linear-gradient(
525
+ 90deg,
526
+ var(--skeleton-base-color) 25%,
527
+ var(--skeleton-highlight-color) 37%,
528
+ var(--skeleton-base-color) 63%
529
+ );
441
530
  background-size: 400% 100%;
442
531
  animation: skeleton-loading 1.2s ease-in-out infinite;
443
- border-radius: 4px;
532
+ border-radius: var(--dbviewer-border-radius);
444
533
  }
445
- .number-loader {
534
+
535
+ /* Skeleton loader variants */
536
+ .skeleton-loader.number-loader {
446
537
  width: 2.5em;
447
538
  height: 1.5em;
448
539
  margin-bottom: 0.2em;
449
540
  }
450
- .table-cell-loader {
541
+
542
+ .skeleton-loader.table-cell-loader {
451
543
  width: 6em;
452
544
  height: 1.2em;
453
545
  }
454
- .records-loader {
546
+
547
+ .skeleton-loader.records-loader {
455
548
  width: 3em;
456
549
  height: 1.2em;
457
550
  }
458
- .query-cell-loader {
551
+
552
+ .skeleton-loader.query-cell-loader {
459
553
  width: 12em;
460
554
  height: 1.2em;
461
555
  }
462
- .duration-cell-loader {
556
+
557
+ .skeleton-loader.duration-cell-loader {
463
558
  width: 4em;
464
559
  height: 1.2em;
465
560
  }
466
- .time-cell-loader {
561
+
562
+ .skeleton-loader.time-cell-loader {
467
563
  width: 7em;
468
564
  height: 1.2em;
469
565
  }
566
+
567
+ /* ================================================
568
+ Animations
569
+ ================================================ */
470
570
  @keyframes skeleton-loading {
471
571
  0% {
472
572
  background-position: 100% 50%;
@@ -475,4 +575,58 @@ document.addEventListener('DOMContentLoaded', function() {
475
575
  background-position: 0 50%;
476
576
  }
477
577
  }
578
+
579
+ /* ================================================
580
+ Table Enhancements
581
+ ================================================ */
582
+ .table-hover tbody tr:hover .sql-query-code {
583
+ background-color: var(--dbviewer-code-bg);
584
+ filter: brightness(0.9);
585
+ }
586
+
587
+ /* ================================================
588
+ Responsive Design
589
+ ================================================ */
590
+ @media (max-width: 768px) {
591
+ .sql-query-code {
592
+ font-size: 0.75rem;
593
+ padding: 1px 3px;
594
+ }
595
+
596
+ .query-cell-loader {
597
+ width: 8em;
598
+ }
599
+
600
+ .duration-cell-loader {
601
+ width: 3em;
602
+ }
603
+
604
+ .time-cell-loader {
605
+ width: 5em;
606
+ }
607
+ }
608
+
609
+ /* ================================================
610
+ Accessibility Improvements
611
+ ================================================ */
612
+ @media (prefers-reduced-motion: reduce) {
613
+ .skeleton-loader {
614
+ animation: none;
615
+ background: var(--skeleton-base-color);
616
+ }
617
+
618
+ .sql-query-code,
619
+ .query-duration,
620
+ .query-duration-slow,
621
+ .query-timestamp,
622
+ .empty-data-message {
623
+ transition: none;
624
+ }
625
+ }
626
+
627
+ /* Focus states for better keyboard navigation */
628
+ .sql-query-code:focus-visible {
629
+ outline: 2px solid var(--dbviewer-success-color);
630
+ outline-offset: 2px;
631
+ }
478
632
  </style>