dbviewer 0.4.8 → 0.5.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.
- checksums.yaml +4 -4
- data/README.md +4 -20
- data/app/controllers/concerns/dbviewer/database_operations.rb +28 -75
- data/app/controllers/dbviewer/api/base_controller.rb +19 -0
- data/app/controllers/dbviewer/api/database_controller.rb +10 -0
- data/app/controllers/dbviewer/api/queries_controller.rb +28 -0
- data/app/controllers/dbviewer/api/tables_controller.rb +64 -0
- data/app/controllers/dbviewer/application_controller.rb +1 -1
- data/app/controllers/dbviewer/home_controller.rb +1 -93
- data/app/controllers/dbviewer/tables_controller.rb +2 -2
- data/app/views/dbviewer/home/index.html.erb +6 -6
- data/app/views/dbviewer/tables/show.html.erb +135 -2
- data/app/views/layouts/dbviewer/application.html.erb +184 -84
- data/app/views/layouts/dbviewer/shared/_sidebar.html.erb +0 -1
- data/config/routes.rb +18 -7
- data/lib/dbviewer/database_manager.rb +40 -0
- data/lib/dbviewer/engine.rb +15 -0
- data/lib/dbviewer/logger.rb +0 -1
- data/lib/dbviewer/query_parser.rb +0 -10
- data/lib/dbviewer/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d16f8924e91f06f61088b4948397aa23530cd6e4a89e00ab483cc047ea5fc492
|
4
|
+
data.tar.gz: 98091d292dfe7c42f0644dc1734fb298f532cfc6f99e0176ff1ae517c8db1cc8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a423fec219eaf8f40adde10a316cd4e45f33eb27d514ead5a0da735db3a475ac1ac430851cc347095106ca011dda72eae4ab68a01cba8ea8782ab132c99de8b6
|
7
|
+
data.tar.gz: 57594e053bf4bd1389da099fa91236af80e579bfc6f0ebd72da5081187c16a2343717c803632781bf39eb8d5281151cc8614def62c035b1b35dfd2f8946da631
|
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/
|
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
|
-
|
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
|
@@ -5,7 +5,7 @@ module Dbviewer
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
helper_method :current_table?, :get_database_name if respond_to?(:helper_method)
|
8
|
+
helper_method :current_table?, :get_database_name, :get_adapter_name if respond_to?(:helper_method)
|
9
9
|
end
|
10
10
|
|
11
11
|
# Initialize the database manager
|
@@ -44,13 +44,36 @@ module Dbviewer
|
|
44
44
|
"Database"
|
45
45
|
end
|
46
46
|
|
47
|
+
# Get the name of the current database adapter
|
48
|
+
def get_adapter_name
|
49
|
+
adapter = database_manager.connection.adapter_name
|
50
|
+
|
51
|
+
# Format the adapter name for better display
|
52
|
+
case adapter.downcase
|
53
|
+
when /mysql/
|
54
|
+
"MySQL"
|
55
|
+
when /postgres/
|
56
|
+
"PostgreSQL"
|
57
|
+
when /sqlite/
|
58
|
+
"SQLite"
|
59
|
+
when /oracle/
|
60
|
+
"Oracle"
|
61
|
+
when /sqlserver/, /mssql/
|
62
|
+
"SQL Server"
|
63
|
+
else
|
64
|
+
adapter.titleize # Fallback to titleized version
|
65
|
+
end
|
66
|
+
rescue => e
|
67
|
+
Rails.logger.error("Error retrieving adapter name: #{e.message}")
|
68
|
+
"Unknown"
|
69
|
+
end
|
70
|
+
|
47
71
|
# Fetch all tables with their stats
|
48
72
|
# By default, don't include record counts for better performance on sidebar
|
49
|
-
def
|
73
|
+
def fetch_tables(include_record_counts = false)
|
50
74
|
database_manager.tables.map do |table_name|
|
51
75
|
table_stats = {
|
52
76
|
name: table_name
|
53
|
-
# columns_count: database_manager.column_count(table_name)
|
54
77
|
}
|
55
78
|
|
56
79
|
# Only fetch record counts if explicitly requested
|
@@ -63,7 +86,7 @@ module Dbviewer
|
|
63
86
|
# Gather database analytics information
|
64
87
|
def fetch_database_analytics
|
65
88
|
# For analytics, we do need record counts
|
66
|
-
tables =
|
89
|
+
tables = fetch_tables(include_record_counts: true)
|
67
90
|
|
68
91
|
# Calculate overall statistics
|
69
92
|
analytics = {
|
@@ -72,82 +95,12 @@ module Dbviewer
|
|
72
95
|
largest_tables: tables.sort_by { |t| -t[:record_count] }.first(10),
|
73
96
|
empty_tables: tables.select { |t| t[:record_count] == 0 }
|
74
97
|
}
|
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
98
|
# Calculate schema size if possible
|
90
|
-
|
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
|
99
|
+
analytics[:schema_size] = calculate_schema_size
|
105
100
|
|
106
101
|
analytics
|
107
102
|
end
|
108
103
|
|
109
|
-
# Calculate approximate schema size
|
110
|
-
def calculate_schema_size
|
111
|
-
adapter = database_manager.connection.adapter_name.downcase
|
112
|
-
|
113
|
-
case adapter
|
114
|
-
when /mysql/
|
115
|
-
query = <<-SQL
|
116
|
-
SELECT
|
117
|
-
SUM(data_length + index_length) AS size
|
118
|
-
FROM
|
119
|
-
information_schema.TABLES
|
120
|
-
WHERE
|
121
|
-
table_schema = DATABASE()
|
122
|
-
SQL
|
123
|
-
result = database_manager.execute_query(query).first
|
124
|
-
result ? result["size"].to_i : nil
|
125
|
-
when /postgres/
|
126
|
-
query = <<-SQL
|
127
|
-
SELECT pg_database_size(current_database()) AS size
|
128
|
-
SQL
|
129
|
-
result = database_manager.execute_query(query).first
|
130
|
-
result ? result["size"].to_i : nil
|
131
|
-
when /sqlite/
|
132
|
-
# For SQLite, we need to use the special PRAGMA method without LIMIT
|
133
|
-
# Get page count
|
134
|
-
page_count_result = database_manager.execute_sqlite_pragma("page_count")
|
135
|
-
page_count = page_count_result.first.values.first.to_i
|
136
|
-
|
137
|
-
# Get page size
|
138
|
-
page_size_result = database_manager.execute_sqlite_pragma("page_size")
|
139
|
-
page_size = page_size_result.first.values.first.to_i
|
140
|
-
|
141
|
-
# Calculate total size
|
142
|
-
page_count * page_size
|
143
|
-
else
|
144
|
-
nil # Unsupported database type for size calculation
|
145
|
-
end
|
146
|
-
rescue => e
|
147
|
-
Rails.logger.error("Error calculating database size: #{e.message}")
|
148
|
-
nil
|
149
|
-
end
|
150
|
-
|
151
104
|
# Get column information for a specific table
|
152
105
|
def fetch_table_columns(table_name)
|
153
106
|
database_manager.table_columns(table_name)
|
@@ -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,28 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module Api
|
3
|
+
class QueriesController < BaseController
|
4
|
+
def recent
|
5
|
+
render_success({
|
6
|
+
enabled: query_logging_enabled?,
|
7
|
+
queries: fetch_recent_queries
|
8
|
+
})
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def fetch_recent_queries
|
14
|
+
return [] unless query_logging_enabled?
|
15
|
+
|
16
|
+
Dbviewer::Logger.instance.recent_queries(limit: queries_limit)
|
17
|
+
end
|
18
|
+
|
19
|
+
def query_logging_enabled?
|
20
|
+
Dbviewer.configuration.enable_query_logging
|
21
|
+
end
|
22
|
+
|
23
|
+
def queries_limit
|
24
|
+
10
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module Api
|
3
|
+
class TablesController < BaseController
|
4
|
+
def index
|
5
|
+
tables_count = fetch_tables_count
|
6
|
+
render_success(total_tables: tables_count)
|
7
|
+
end
|
8
|
+
|
9
|
+
def records
|
10
|
+
tables_stats = fetch_tables_stats
|
11
|
+
render_success(tables_stats)
|
12
|
+
end
|
13
|
+
|
14
|
+
def relationships_count
|
15
|
+
total_relationships = calculate_total_relationships
|
16
|
+
render_success(total_relationships: total_relationships)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def fetch_tables_count
|
22
|
+
fetch_tables(include_record_counts: false).size
|
23
|
+
end
|
24
|
+
|
25
|
+
def fetch_tables_stats
|
26
|
+
tables = fetch_tables(include_record_counts: true)
|
27
|
+
|
28
|
+
{
|
29
|
+
total_records: calculate_total_records(tables),
|
30
|
+
largest_tables: find_largest_tables(tables),
|
31
|
+
empty_tables: find_empty_tables(tables),
|
32
|
+
avg_records_per_table: calculate_average_records(tables)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def calculate_total_relationships
|
37
|
+
tables = fetch_tables(include_record_counts: false)
|
38
|
+
|
39
|
+
tables.sum do |table|
|
40
|
+
metadata = fetch_table_metadata(table[:name])
|
41
|
+
metadata&.dig(:foreign_keys)&.size || 0
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def calculate_total_records(tables)
|
46
|
+
tables.sum { |table| table[:record_count] }
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_largest_tables(tables, limit = 10)
|
50
|
+
tables.sort_by { |table| -table[:record_count] }.first(limit)
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_empty_tables(tables)
|
54
|
+
tables.select { |table| table[:record_count] == 0 }
|
55
|
+
end
|
56
|
+
|
57
|
+
def calculate_average_records(tables)
|
58
|
+
return 0 if tables.empty?
|
59
|
+
|
60
|
+
(calculate_total_records(tables).to_f / tables.size).round(1)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,104 +1,12 @@
|
|
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
|
99
7
|
|
100
8
|
def set_tables
|
101
|
-
@tables =
|
9
|
+
@tables = fetch_tables(include_record_counts: true)
|
102
10
|
end
|
103
11
|
end
|
104
12
|
end
|
@@ -7,7 +7,7 @@ module Dbviewer
|
|
7
7
|
before_action :set_global_filters, only: [ :show, :export_csv ]
|
8
8
|
|
9
9
|
def index
|
10
|
-
@tables =
|
10
|
+
@tables = fetch_tables(include_record_counts: true)
|
11
11
|
end
|
12
12
|
|
13
13
|
def show
|
@@ -60,7 +60,7 @@ module Dbviewer
|
|
60
60
|
def query
|
61
61
|
@read_only_mode = true # Flag to indicate we're in read-only mode
|
62
62
|
@columns = fetch_table_columns(@table_name)
|
63
|
-
@tables =
|
63
|
+
@tables = fetch_tables # Fetch tables for sidebar
|
64
64
|
|
65
65
|
prepare_query
|
66
66
|
execute_query
|
@@ -3,12 +3,12 @@
|
|
3
3
|
<div class="col">
|
4
4
|
<h1 class="h3 mb-2">Database Overview</h1>
|
5
5
|
<p class="database-connection-info">
|
6
|
-
Connected to database: <span class="badge rounded-pill database-name-badge"><%= get_database_name %></span>
|
6
|
+
Connected to <%= get_adapter_name %> database: <span class="badge rounded-pill database-name-badge"><%= get_database_name %></span>
|
7
7
|
</p>
|
8
8
|
</div>
|
9
9
|
</div>
|
10
10
|
|
11
|
-
<div class="row g-3 mb-4
|
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('<%=
|
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('<%=
|
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('<%=
|
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 %>
|