dbviewer 0.4.8 → 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: 03dfb13567e8cf7a2eaf2977e421fec7a2117ff12a6462515399a4b7cfb056d1
4
- data.tar.gz: 77ae23ada80eabbc1a5744376a96c0d5df4f20f0cb3723be4c0f9c9967994716
3
+ metadata.gz: 628884438be23468b7910e2a13c14b2aa00cc6361f01cb09b53ecdcf1d8a0b78
4
+ data.tar.gz: e9952d0c11ae3669b2604ac208f1a4ae224c37eb4d5f2b1074c4f284bb0bc24a
5
5
  SHA512:
6
- metadata.gz: 35d69706133dc764c081a6bc172dc0d99f39c2eaa7d62db9c17664c5a1fe05b78255932bd14a3f9a9542e99cb74020cb2926d69c9d21d9a339776fc4023098e9
7
- data.tar.gz: b3a4e2029d81da79e922fb5ee0e889fc9e1f66e66129136333f6820d66dbc1dd7648da63a4ab99d3ed822e8cc64c77e9815a1c1bd34b4d0a11b208118d2eb72c
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
@@ -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'
@@ -314,7 +314,7 @@
314
314
  <% content_for :sidebar_active do %>active<% end %>
315
315
 
316
316
  <div class="d-flex justify-content-between align-items-center mb-4">
317
- <div>
317
+ <div class="d-flex justify-content-between align-items-center">
318
318
  <h1>Table: <%= @table_name %></h1>
319
319
  </div>
320
320
  <div class="d-flex gap-2">
@@ -393,7 +393,7 @@
393
393
  </div>
394
394
 
395
395
  <!-- Records Section -->
396
- <div class="dbviewer-card card mb-4">
396
+ <div class="dbviewer-card card mb-4" id="table-section">
397
397
  <div class="card-header d-flex justify-content-between align-items-center">
398
398
  <h5 class="mb-0">
399
399
  <select id="per-page-select" class="form-select form-select-sm" onchange="window.location.href='<%= table_path(@table_name) %>?<%= per_page_url_params(@table_name) %>'">
@@ -414,6 +414,9 @@
414
414
  <% if active_filters > 0 %>
415
415
  <span class="badge bg-info ms-2" title="Active filters"><i class="bi bi-funnel-fill me-1"></i><%= active_filters %></span>
416
416
  <% end %>
417
+ <button type="button" class="btn btn-outline-secondary btn-sm ms-2" id="fullscreen-toggle" title="Toggle fullscreen">
418
+ <i class="bi bi-fullscreen" id="fullscreen-icon"></i>
419
+ </button>
417
420
  </div>
418
421
  </div>
419
422
  <div class="card-body p-0">
@@ -1311,6 +1314,71 @@
1311
1314
  [data-bs-theme="dark"] .record-detail-table .code-block {
1312
1315
  background-color: var(--bs-dark);
1313
1316
  }
1317
+
1318
+ /* Fullscreen table styles */
1319
+ .table-fullscreen {
1320
+ position: fixed !important;
1321
+ top: 0 !important;
1322
+ left: 0 !important;
1323
+ width: 100vw !important;
1324
+ height: 100vh !important;
1325
+ z-index: 9999 !important;
1326
+ background: var(--bs-body-bg) !important;
1327
+ margin: 0 !important;
1328
+ border-radius: 0 !important;
1329
+ overflow: hidden !important;
1330
+ display: flex !important;
1331
+ flex-direction: column !important;
1332
+ }
1333
+
1334
+ .table-fullscreen .card-body {
1335
+ flex: 1 !important;
1336
+ overflow: hidden !important;
1337
+ display: flex !important;
1338
+ flex-direction: column !important;
1339
+ }
1340
+
1341
+ .table-fullscreen .table-responsive {
1342
+ flex: 1 !important;
1343
+ overflow: auto !important;
1344
+ }
1345
+
1346
+ .table-fullscreen .card-header {
1347
+ flex-shrink: 0 !important;
1348
+ position: sticky !important;
1349
+ top: 0 !important;
1350
+ z-index: 10000 !important;
1351
+ background: var(--bs-body-bg) !important;
1352
+ border-bottom: 1px solid var(--bs-border-color) !important;
1353
+ }
1354
+
1355
+ /* Hide pagination in fullscreen mode */
1356
+ .table-fullscreen .pagination-container {
1357
+ display: none !important;
1358
+ }
1359
+
1360
+ /* Adjust table header in fullscreen */
1361
+ .table-fullscreen .dbviewer-table-header {
1362
+ position: sticky !important;
1363
+ top: 0 !important;
1364
+ z-index: 100 !important;
1365
+ }
1366
+
1367
+ /* Ensure body doesn't scroll when table is fullscreen */
1368
+ body.table-fullscreen-active {
1369
+ overflow: hidden !important;
1370
+ }
1371
+
1372
+ /* Fullscreen button hover effect */
1373
+ #fullscreen-toggle:hover {
1374
+ background-color: var(--bs-secondary-bg) !important;
1375
+ border-color: var(--bs-secondary-border-subtle) !important;
1376
+ }
1377
+
1378
+ /* Smooth transitions */
1379
+ #table-section {
1380
+ transition: all 0.3s ease-in-out;
1381
+ }
1314
1382
  </style>
1315
1383
 
1316
1384
  <% if @timestamp_data.present? %>
@@ -1516,5 +1584,70 @@
1516
1584
 
1517
1585
  return section;
1518
1586
  }
1587
+
1588
+ // Table fullscreen functionality
1589
+ document.addEventListener('DOMContentLoaded', function() {
1590
+ const fullscreenToggle = document.getElementById('fullscreen-toggle');
1591
+ const fullscreenIcon = document.getElementById('fullscreen-icon');
1592
+ const tableSection = document.getElementById('table-section');
1593
+
1594
+ if (fullscreenToggle && tableSection) {
1595
+ // Key for storing fullscreen state in localStorage
1596
+ const fullscreenStateKey = 'dbviewer-table-fullscreen-<%= @table_name %>';
1597
+
1598
+ // Function to apply fullscreen state
1599
+ function applyFullscreenState(isFullscreen) {
1600
+ if (isFullscreen) {
1601
+ // Enter fullscreen
1602
+ tableSection.classList.add('table-fullscreen');
1603
+ document.body.classList.add('table-fullscreen-active');
1604
+ fullscreenIcon.classList.remove('bi-fullscreen');
1605
+ fullscreenIcon.classList.add('bi-fullscreen-exit');
1606
+ fullscreenToggle.setAttribute('title', 'Exit fullscreen');
1607
+ } else {
1608
+ // Exit fullscreen
1609
+ tableSection.classList.remove('table-fullscreen');
1610
+ document.body.classList.remove('table-fullscreen-active');
1611
+ fullscreenIcon.classList.remove('bi-fullscreen-exit');
1612
+ fullscreenIcon.classList.add('bi-fullscreen');
1613
+ fullscreenToggle.setAttribute('title', 'Toggle fullscreen');
1614
+ }
1615
+ }
1616
+
1617
+ // Restore fullscreen state from localStorage on page load
1618
+ try {
1619
+ const savedState = localStorage.getItem(fullscreenStateKey);
1620
+ if (savedState === 'true') {
1621
+ applyFullscreenState(true);
1622
+ }
1623
+ } catch (e) {
1624
+ // Handle localStorage not available (private browsing, etc.)
1625
+ console.warn('Could not restore fullscreen state:', e);
1626
+ }
1627
+
1628
+ fullscreenToggle.addEventListener('click', function() {
1629
+ const isFullscreen = tableSection.classList.contains('table-fullscreen');
1630
+ const newState = !isFullscreen;
1631
+
1632
+ // Apply the new state
1633
+ applyFullscreenState(newState);
1634
+
1635
+ // Save state to localStorage
1636
+ try {
1637
+ localStorage.setItem(fullscreenStateKey, newState.toString());
1638
+ } catch (e) {
1639
+ // Handle localStorage not available (private browsing, etc.)
1640
+ console.warn('Could not save fullscreen state:', e);
1641
+ }
1642
+ });
1643
+
1644
+ // Exit fullscreen with Escape key
1645
+ document.addEventListener('keydown', function(e) {
1646
+ if (e.key === 'Escape' && tableSection.classList.contains('table-fullscreen')) {
1647
+ fullscreenToggle.click();
1648
+ }
1649
+ });
1650
+ }
1651
+ });
1519
1652
  </script>
1520
1653
  <% end %>
@@ -32,9 +32,11 @@
32
32
  /* Base styles and typography */
33
33
  body {
34
34
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
35
- font-size: 0.95rem;
35
+ font-size: 0.875rem;
36
36
  line-height: 1.5;
37
37
  letter-spacing: -0.01em;
38
+ color: #464c54; /* More subdued text color for light theme */
39
+ background-color: #f5f6f9; /* Subtle gray background */
38
40
  }
39
41
 
40
42
  h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
@@ -43,11 +45,11 @@
43
45
  line-height: 1.25;
44
46
  }
45
47
 
46
- /* Core layout styles */
48
+ /* Core layout styles - Grafana-like compact layout */
47
49
  .dbviewer-wrapper { display: flex; flex-direction: column; min-height: 100vh; }
48
- .dbviewer-navbar { height: 60px; }
49
- .dbviewer-navbar-spacer { height: 60px; } /* Creates space for the fixed navbar */
50
- .dbviewer-content { display: flex; flex: 1; min-height: calc(100vh - 60px); padding-top: 0; }
50
+ .dbviewer-navbar { height: 48px; } /* Reduced height for more compact header */
51
+ .dbviewer-navbar-spacer { height: 48px; } /* Creates space for the fixed navbar */
52
+ .dbviewer-content { display: flex; flex: 1; min-height: calc(100vh - 48px); padding-top: 0; }
51
53
 
52
54
  /* Smooth theme transitions */
53
55
  html {
@@ -67,19 +69,19 @@
67
69
  transition: color 0.2s ease, background-color 0.2s ease;
68
70
  }
69
71
 
70
- /* Sidebar styles - enhanced for elegance */
72
+ /* Sidebar styles - enhanced for elegance with Grafana-like compact design */
71
73
  .dbviewer-sidebar {
72
- width: 260px;
73
- height: calc(100vh - 60px);
74
+ width: 240px; /* More compact sidebar width */
75
+ height: calc(100vh - 48px);
74
76
  position: fixed;
75
- top: 60px; /* Positioned right below the fixed navbar */
77
+ top: 48px; /* Positioned right below the fixed navbar */
76
78
  left: 0;
77
79
  z-index: 1000;
78
80
  display: flex;
79
81
  flex-direction: column;
80
82
  transition: transform 0.3s ease-in-out, background-color 0.2s ease, border-color 0.2s ease;
81
- overflow: hidden; /* Changed from overflow-y: auto */
82
- box-shadow: 3px 0 20px rgba(0, 0, 0, 0.05);
83
+ overflow: hidden;
84
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.04); /* Subtler shadow */
83
85
  }
84
86
 
85
87
  /* Dark mode overrides */
@@ -94,7 +96,7 @@
94
96
  }
95
97
 
96
98
  .dbviewer-sidebar-header {
97
- padding: 1.2rem 1.25rem;
99
+ padding: 0.8rem 1rem; /* Reduced padding for more compact header */
98
100
  font-weight: 600;
99
101
  align-items: center;
100
102
  justify-content: space-between;
@@ -157,7 +159,7 @@
157
159
  .dbviewer-sidebar-content {
158
160
  flex: 1;
159
161
  overflow-y: auto;
160
- padding: 0.5rem 0;
162
+ padding: 0.25rem 0; /* Reduced padding */
161
163
  height: 100%;
162
164
  /* Improved scrollbar */
163
165
  scrollbar-width: thin;
@@ -183,9 +185,9 @@
183
185
 
184
186
  .dbviewer-main {
185
187
  flex: 1;
186
- margin-left: 260px;
187
- padding: 2rem 2.5rem;
188
- padding-top: 1.5rem; /* Adjusted for fixed header */
188
+ margin-left: 240px; /* Reduced sidebar width */
189
+ padding: 1.2rem 1.5rem; /* Reduced padding for more compact look */
190
+ padding-top: 0.8rem; /* Adjusted for fixed header */
189
191
  min-width: 0;
190
192
  animation: fadeIn 0.5s ease-in-out;
191
193
  transition: all 0.3s ease;
@@ -281,19 +283,20 @@
281
283
  }
282
284
  }
283
285
 
284
- /* Extra small screens */
286
+ /* Extra small screens - more compact for small devices */
285
287
  @media (max-width: 575.98px) {
286
- .dbviewer-main { padding: 0.75rem; }
288
+ .dbviewer-main { padding: 0.6rem; }
287
289
  h1, .h1 { font-size: 1.6rem; }
288
290
  h2, .h2 { font-size: 1.4rem; }
289
291
  h3, .h3 { font-size: 1.2rem; }
290
292
 
291
293
  .btn-sm {
292
- font-size: 0.8rem;
294
+ font-size: 0.75rem;
295
+ padding: 0.2rem 0.5rem;
293
296
  }
294
297
 
295
298
  .card-body {
296
- padding: 1rem;
299
+ padding: 0.75rem;
297
300
  }
298
301
  }
299
302
 
@@ -343,30 +346,31 @@
343
346
  border-color: rgba(134, 183, 254, 0.7);
344
347
  }
345
348
 
346
- /* Table structure and visualization components */
349
+ /* Table structure and visualization components - Grafana-inspired */
347
350
  .dbviewer-card {
348
- border-radius: 0.5rem;
351
+ border-radius: 3px; /* Smaller border radius for Grafana look */
349
352
  transition: all 0.2s ease;
350
353
  overflow: hidden;
351
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.08);
354
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.05); /* Subtler shadow */
352
355
  height: 100%;
353
356
  }
354
357
 
355
358
  .dbviewer-card:hover {
356
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08), 0 1px 6px rgba(0, 0, 0, 0.1);
357
- transform: translateY(-2px);
359
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.07), 0 1px 3px rgba(0, 0, 0, 0.06);
360
+ transform: translateY(-1px); /* Subtler hover effect */
358
361
  }
359
362
 
360
- /* Improved card headers */
363
+ /* Improved card headers - more compact */
361
364
  .dbviewer-card .card-header {
362
365
  font-weight: 600;
363
- padding: 1rem 1.25rem;
366
+ padding: 0.7rem 1rem; /* Reduced padding */
364
367
  border-bottom-width: 1px;
368
+ font-size: 0.95rem; /* Slightly smaller font */
365
369
  }
366
370
 
367
- /* Card body padding */
371
+ /* Card body padding - more compact */
368
372
  .dbviewer-card .card-body {
369
- padding: 1.25rem;
373
+ padding: 0.9rem 1rem; /* Reduced padding */
370
374
  }
371
375
 
372
376
  [data-bs-theme="light"] .dbviewer-card {
@@ -483,7 +487,7 @@
483
487
  /* List group styling for dark mode */
484
488
  [data-bs-theme="dark"] .list-group-item {
485
489
  background-color: var(--bs-dark);
486
- border-color: rgba(255, 255, 255, 0.15);
490
+ border-color: rgba(255, 255, 255, 0.08); /* Subtler border */
487
491
  color: var(--bs-light);
488
492
  }
489
493
 
@@ -494,10 +498,10 @@
494
498
  z-index: 1;
495
499
  }
496
500
 
497
- /* Equal height for timeline and structure cards */
501
+ /* Equal height for timeline and structure cards - Grafana-inspired compact layout */
498
502
  .two-column-layout .card { height: 100%; display: flex; flex-direction: column; }
499
- .two-column-layout .card-body { flex: 1; display: flex; flex-direction: column; }
500
- .two-column-layout .chart-container { flex: 1; min-height: 250px; }
503
+ .two-column-layout .card-body { flex: 1; display: flex; flex-direction: column; padding: 0.75rem; } /* Reduced padding */
504
+ .two-column-layout .chart-container { flex: 1; min-height: 220px; } /* Reduced min-height */
501
505
  .two-column-layout .structure-container { padding: 0; }
502
506
  .two-column-layout .tab-content { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
503
507
  .two-column-layout .tab-pane { flex: 1; overflow: hidden; }
@@ -560,24 +564,28 @@
560
564
  font-size: 1.2rem;
561
565
  }
562
566
 
563
- /* Header styles for a more professional look */
567
+ /* Header styles for a more professional Grafana-like look */
564
568
  .navbar {
565
569
  transition: all 0.3s ease;
566
- padding: 0.5rem 1rem;
567
- box-shadow: 0 2px 15px rgba(0, 0, 0, 0.08);
570
+ padding: 0 0.75rem; /* Reduced horizontal padding */
571
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); /* Subtler shadow */
572
+ min-height: 48px; /* Fixed height */
568
573
  }
569
574
 
570
575
  .navbar-brand {
571
576
  font-weight: 600;
572
577
  letter-spacing: -0.01em;
578
+ font-size: 1.1rem; /* Smaller brand */
579
+ padding: 0.1rem 0; /* Reduced padding */
573
580
  }
574
581
 
575
582
  .nav-link {
576
583
  font-weight: 500;
577
- padding: 0.5rem 0.8rem !important;
578
- border-radius: 4px;
584
+ padding: 0.35rem 0.65rem !important; /* Reduced padding */
585
+ border-radius: 2px; /* Smaller radius for Grafana look */
579
586
  transition: all 0.2s ease;
580
- margin: 0 2px;
587
+ margin: 0 1px; /* Reduced margin */
588
+ font-size: 0.9rem; /* Smaller font */
581
589
  }
582
590
 
583
591
  .nav-link.active {
@@ -602,11 +610,11 @@
602
610
  border-radius: 0;
603
611
  border-left: 0;
604
612
  border-right: 0;
605
- padding: 0.75rem 1.25rem;
613
+ padding: 0.5rem 0.85rem; /* Reduced padding for compact look */
606
614
  transition: all 0.15s ease-in-out;
607
615
  position: relative;
608
616
  font-weight: 500;
609
- font-size: 0.9rem;
617
+ font-size: 0.85rem; /* Smaller font for Grafana-like appearance */
610
618
  }
611
619
 
612
620
  .list-group-item:first-child {
@@ -679,23 +687,23 @@
679
687
  color: #adb5bd;
680
688
  }
681
689
 
682
- /* Enhanced button styling */
690
+ /* Enhanced button styling - Grafana-inspired */
683
691
  .btn {
684
692
  font-weight: 500;
685
- padding: 0.5rem 1rem;
686
- border-radius: 0.375rem;
693
+ padding: 0.4rem 0.85rem; /* Reduced padding */
694
+ border-radius: 2px; /* Smaller radius for Grafana look */
687
695
  transition: all 0.2s ease;
688
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
696
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
689
697
  }
690
698
 
691
699
  .btn-sm {
692
- padding: 0.35rem 0.75rem;
693
- font-size: 0.85rem;
700
+ padding: 0.25rem 0.6rem; /* Reduced padding */
701
+ font-size: 0.8rem; /* Smaller font size */
694
702
  }
695
703
 
696
704
  .btn-lg {
697
- padding: 0.65rem 1.2rem;
698
- font-size: 1.05rem;
705
+ padding: 0.5rem 1rem; /* Reduced padding */
706
+ font-size: 0.95rem; /* Smaller font size */
699
707
  }
700
708
 
701
709
  .btn:active {
@@ -760,23 +768,23 @@
760
768
  text-decoration: underline;
761
769
  }
762
770
 
763
- /* Enhanced code blocks and SQL query styling */
771
+ /* Enhanced code blocks and SQL query styling - Grafana-inspired */
764
772
  pre, code {
765
773
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
766
- border-radius: 4px;
774
+ border-radius: 2px; /* Smaller radius */
767
775
  transition: all 0.2s ease;
768
776
  }
769
777
 
770
778
  pre {
771
- padding: 1rem;
772
- margin-bottom: 1rem;
773
- border-radius: 0.5rem;
774
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
779
+ padding: 0.75rem; /* Reduced padding */
780
+ margin-bottom: 0.75rem; /* Reduced margin */
781
+ border-radius: 2px; /* Smaller radius */
782
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
775
783
  }
776
784
 
777
785
  code {
778
- font-size: 0.875em;
779
- padding: 0.2em 0.4em;
786
+ font-size: 0.825em; /* Smaller font */
787
+ padding: 0.15em 0.3em; /* Reduced padding */
780
788
  }
781
789
 
782
790
  /* SQL query code in tables */
@@ -868,7 +876,7 @@
868
876
  color: #ffffff;
869
877
  }
870
878
 
871
- /* Enhanced table styling */
879
+ /* Enhanced table styling - Grafana-like compact design */
872
880
  .table {
873
881
  margin-bottom: 0;
874
882
  width: 100%;
@@ -881,12 +889,12 @@
881
889
  }
882
890
 
883
891
  .table thead th {
884
- padding: 1rem;
892
+ padding: 0.65rem 0.75rem; /* Reduced padding */
885
893
  font-weight: 600;
886
894
  border-top: 0;
887
895
  vertical-align: middle;
888
896
  letter-spacing: 0.01em;
889
- font-size: 0.88rem;
897
+ font-size: 0.78rem; /* Smaller font */
890
898
  text-transform: uppercase;
891
899
  position: sticky;
892
900
  top: 0;
@@ -895,10 +903,10 @@
895
903
  }
896
904
 
897
905
  .table tbody td {
898
- padding: 0.85rem 1rem;
906
+ padding: 0.5rem 0.75rem; /* Reduced padding */
899
907
  vertical-align: middle;
900
908
  border-color: var(--bs-border-color);
901
- font-size: 0.92rem;
909
+ font-size: 0.85rem; /* Smaller font */
902
910
  }
903
911
 
904
912
  .table tbody tr:hover {
@@ -960,14 +968,21 @@
960
968
  color: #000 !important;
961
969
  }
962
970
 
963
- /* Enhanced alert styling */
971
+ /* Badge styling - More Grafana-like */
972
+ .badge {
973
+ padding: 0.3em 0.5em;
974
+ font-size: 0.75em;
975
+ border-radius: 2px;
976
+ }
977
+
978
+ /* Enhanced alert styling - Grafana-inspired */
964
979
  .alert {
965
- border-radius: 0.5rem;
980
+ border-radius: 2px; /* Smaller radius for Grafana look */
966
981
  border-width: 0;
967
- padding: 1rem 1.25rem;
968
- box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
982
+ padding: 0.7rem 1rem; /* Reduced padding */
983
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
969
984
  position: relative;
970
- border-left: 4px solid transparent;
985
+ border-left: 3px solid transparent; /* Thinner border */
971
986
  }
972
987
 
973
988
  .alert-info {
@@ -1091,40 +1106,40 @@
1091
1106
  background-color: var(--bs-light);
1092
1107
  }
1093
1108
 
1094
- /* Enhanced stat cards and metric icons */
1109
+ /* Enhanced stat cards and metric icons - Grafana-inspired */
1095
1110
  .stat-card-bg {
1096
1111
  background: #ffffff;
1097
- border-radius: 0.75rem;
1098
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
1112
+ border-radius: 3px; /* Smaller radius for Grafana look */
1113
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.03);
1099
1114
  transition: all 0.3s ease;
1100
1115
  border: none;
1101
1116
  }
1102
1117
 
1103
1118
  .stat-card-bg:hover {
1104
- transform: translateY(-3px);
1105
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
1119
+ transform: translateY(-1px); /* Subtler hover */
1120
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
1106
1121
  }
1107
1122
 
1108
1123
  [data-bs-theme="dark"] .stat-card-bg {
1109
1124
  background-color: #212529;
1110
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
1125
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
1111
1126
  }
1112
1127
 
1113
- /* Enhanced metric icon styling */
1128
+ /* Enhanced metric icon styling - Grafana-inspired */
1114
1129
  .metric-icon {
1115
1130
  display: flex;
1116
1131
  align-items: center;
1117
1132
  justify-content: center;
1118
- border-radius: 14px;
1119
- width: 65px;
1120
- height: 65px;
1121
- min-width: 65px;
1133
+ border-radius: 3px; /* Smaller radius */
1134
+ width: 50px; /* Smaller icon */
1135
+ height: 50px; /* Smaller icon */
1136
+ min-width: 50px; /* Smaller icon */
1122
1137
  text-align: center;
1123
- background: linear-gradient(135deg, rgba(var(--bs-primary-rgb), 0.2) 0%, rgba(var(--bs-primary-rgb), 0.1) 100%);
1138
+ background: linear-gradient(135deg, rgba(var(--bs-primary-rgb), 0.15) 0%, rgba(var(--bs-primary-rgb), 0.08) 100%);
1124
1139
  color: var(--bs-primary);
1125
- font-size: 1.6rem;
1140
+ font-size: 1.4rem; /* Smaller icon */
1126
1141
  transition: all 0.3s ease;
1127
- box-shadow: 0 3px 10px rgba(var(--bs-primary-rgb), 0.1);
1142
+ box-shadow: 0 1px 5px rgba(var(--bs-primary-rgb), 0.08);
1128
1143
  }
1129
1144
 
1130
1145
  .stat-card-bg:hover .metric-icon {
@@ -1181,17 +1196,28 @@
1181
1196
  transition: all 0.2s ease;
1182
1197
  }
1183
1198
 
1184
- /* Improved table hover effect */
1199
+ /* Improved table hover effect - Grafana-like */
1185
1200
  .table-hover > tbody > tr:hover {
1186
1201
  transition: all 0.15s ease;
1187
1202
  }
1188
1203
 
1189
1204
  [data-bs-theme="light"] .table-hover > tbody > tr:hover {
1190
- background-color: rgba(13, 110, 253, 0.04);
1205
+ background-color: rgba(13, 110, 253, 0.03); /* Subtler hover */
1191
1206
  }
1192
1207
 
1193
1208
  [data-bs-theme="dark"] .table-hover > tbody > tr:hover {
1194
- background-color: rgba(13, 110, 253, 0.07);
1209
+ background-color: rgba(13, 110, 253, 0.05); /* Subtler hover */
1210
+ }
1211
+
1212
+ /* Grafana-inspired grid layout adjustments */
1213
+ .row {
1214
+ margin-right: -0.5rem;
1215
+ margin-left: -0.5rem;
1216
+ }
1217
+
1218
+ .row > [class^="col-"] {
1219
+ padding-right: 0.5rem;
1220
+ padding-left: 0.5rem;
1195
1221
  }
1196
1222
 
1197
1223
  /* Button press effect */
@@ -1228,6 +1254,80 @@
1228
1254
  [data-bs-theme="dark"] html::-webkit-scrollbar-thumb {
1229
1255
  background-color: rgba(255, 255, 255, 0.2);
1230
1256
  }
1257
+
1258
+ /* Grafana-like panel enhancements */
1259
+ body {
1260
+ color: #464c54; /* More subdued text color for light theme */
1261
+ background-color: #f5f6f9; /* Subtle gray background */
1262
+ }
1263
+
1264
+ [data-bs-theme="dark"] body {
1265
+ background-color: #161719; /* Darker background for dark theme */
1266
+ color: #d8d9da; /* Softer text color for dark theme */
1267
+ }
1268
+
1269
+ /* Make font sizes consistently smaller for more compact look */
1270
+ body {
1271
+ font-size: 0.875rem;
1272
+ }
1273
+
1274
+ h1, .h1 { font-size: 1.6rem; }
1275
+ h2, .h2 { font-size: 1.4rem; }
1276
+ h3, .h3 { font-size: 1.2rem; }
1277
+ h4, .h4 { font-size: 1.1rem; }
1278
+ h5, .h5 { font-size: 1rem; }
1279
+
1280
+ /* Unify panel appearance */
1281
+ .card, .list-group {
1282
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.06);
1283
+ border: none;
1284
+ border-radius: 2px;
1285
+ }
1286
+
1287
+ [data-bs-theme="dark"] .card,
1288
+ [data-bs-theme="dark"] .list-group {
1289
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
1290
+ background-color: #212124;
1291
+ }
1292
+
1293
+ /* Improved tab styling */
1294
+ .nav-tabs {
1295
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
1296
+ }
1297
+
1298
+ .nav-tabs .nav-link {
1299
+ border: none;
1300
+ border-bottom: 2px solid transparent;
1301
+ background-color: transparent;
1302
+ padding: 0.5rem 0.75rem;
1303
+ margin-bottom: -1px;
1304
+ font-size: 0.9rem;
1305
+ }
1306
+
1307
+ .nav-tabs .nav-link.active {
1308
+ border-bottom-color: var(--bs-primary);
1309
+ color: var(--bs-primary);
1310
+ }
1311
+
1312
+ [data-bs-theme="dark"] .nav-tabs {
1313
+ border-color: rgba(255, 255, 255, 0.1);
1314
+ }
1315
+
1316
+ /* Remove excessive margins between elements */
1317
+ .row + .row,
1318
+ .card + .card {
1319
+ margin-top: 0.75rem;
1320
+ }
1321
+
1322
+ /* Elegant form controls */
1323
+ .form-control, .form-select {
1324
+ padding: 0.4rem 0.6rem;
1325
+ font-size: 0.875rem;
1326
+ height: auto;
1327
+ border-radius: 2px;
1328
+ }
1329
+
1330
+ /* Add this right above the style closing tag */
1231
1331
  </style>
1232
1332
  </head>
1233
1333
  <body>
@@ -1,6 +1,5 @@
1
1
  <div class="dbviewer-sidebar-top">
2
2
  <div class="dbviewer-table-filter-container p-1 mb-0">
3
- <i class="bi bi-search dbviewer-table-filter-icon"></i>
4
3
  <input type="text" class="form-control form-control-sm dbviewer-table-filter mb-0"
5
4
  id="tableSearch" placeholder="Filter tables..." aria-label="Filter tables">
6
5
  </div>
data/config/routes.rb CHANGED
@@ -19,13 +19,24 @@ Dbviewer::Engine.routes.draw do
19
19
  # Homepage and API endpoints
20
20
  get "dashboard", to: "home#index", as: :dashboard
21
21
 
22
- # Analytics API endpoints
23
- get "api/analytics", to: "home#analytics" # Legacy/combined endpoint
24
- get "api/records", to: "home#records"
25
- get "api/tables", to: "home#tables_count"
26
- get "api/relationships", to: "home#relationships_count"
27
- get "api/database_size", to: "home#database_size"
28
- get "api/recent_queries", to: "home#recent_queries"
22
+ namespace :api do
23
+ resources :tables, only: [ :index ] do
24
+ collection do
25
+ get "records"
26
+ get "relationships_count"
27
+ end
28
+ end
29
+
30
+ resource :database, only: [], controller: "database" do
31
+ get "size"
32
+ end
33
+
34
+ resources :queries, only: [] do
35
+ collection do
36
+ get "recent"
37
+ end
38
+ end
39
+ end
29
40
 
30
41
  root to: "home#index"
31
42
  end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.4.8"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -0,0 +1,11 @@
1
+ module Dbviewer
2
+ module Generators
3
+ class StructuredApiGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../templates", __FILE__)
5
+
6
+ def create_initializer
7
+ template "structured_api_initializer.rb", "config/initializers/dbviewer_structured_api.rb"
8
+ end
9
+ end
10
+ end
11
+ end
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.4.8
4
+ version: 0.5.0
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-05-25 00:00:00.000000000 Z
11
+ date: 2025-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -55,6 +55,10 @@ files:
55
55
  - app/controllers/concerns/dbviewer/database_operations.rb
56
56
  - app/controllers/concerns/dbviewer/error_handling.rb
57
57
  - app/controllers/concerns/dbviewer/pagination_concern.rb
58
+ - app/controllers/dbviewer/api/base_controller.rb
59
+ - app/controllers/dbviewer/api/database_controller.rb
60
+ - app/controllers/dbviewer/api/queries_controller.rb
61
+ - app/controllers/dbviewer/api/tables_controller.rb
58
62
  - app/controllers/dbviewer/application_controller.rb
59
63
  - app/controllers/dbviewer/entity_relationship_diagrams_controller.rb
60
64
  - app/controllers/dbviewer/home_controller.rb
@@ -96,6 +100,7 @@ files:
96
100
  - lib/dbviewer/table_query_params.rb
97
101
  - lib/dbviewer/version.rb
98
102
  - lib/generators/dbviewer/initializer_generator.rb
103
+ - lib/generators/dbviewer/structured_api_generator.rb
99
104
  - lib/generators/dbviewer/templates/initializer.rb
100
105
  - lib/tasks/dbviewer_tasks.rake
101
106
  homepage: https://github.com/wailantirajoh/dbviewer