dbviewer 0.3.6 → 0.3.16
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 +34 -2
- data/app/controllers/concerns/dbviewer/database_operations.rb +160 -21
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +8 -0
- data/app/controllers/dbviewer/tables_controller.rb +130 -17
- data/app/helpers/dbviewer/application_helper.rb +73 -0
- data/app/views/dbviewer/home/index.html.erb +37 -16
- data/app/views/dbviewer/shared/_sidebar.html.erb +146 -1
- data/app/views/dbviewer/tables/index.html.erb +7 -1
- data/app/views/dbviewer/tables/mini_erd.html.erb +517 -0
- data/app/views/dbviewer/tables/show.html.erb +918 -40
- data/app/views/layouts/dbviewer/application.html.erb +19 -0
- data/config/routes.rb +1 -0
- data/lib/dbviewer/database_manager.rb +31 -115
- data/lib/dbviewer/query_analyzer.rb +130 -0
- data/lib/dbviewer/table_query_operations.rb +621 -0
- data/lib/dbviewer/table_query_params.rb +39 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +1 -0
- metadata +5 -9
- data/app/services/dbviewer/file_storage.rb +0 -0
- data/app/services/dbviewer/in_memory_storage.rb +0 -0
- data/app/services/dbviewer/query_analyzer.rb +0 -0
- data/app/services/dbviewer/query_collection.rb +0 -0
- data/app/services/dbviewer/query_logger.rb +0 -0
- data/app/services/dbviewer/query_parser.rb +0 -82
- data/app/services/dbviewer/query_storage.rb +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72e7257a74294ca4a6ad8b53fc497ca9eb0727772e798b2c4d7fa2c164b35531
|
4
|
+
data.tar.gz: d01a7c934e3e859f06f94a746671974dd93813e832c6f5f54df49b3071e95178
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79511e9ac29ca71e4834b958c54b36ee05f83e831f1426745282139ea6b46d5e0e1bcd2a2b31df956987218bfc44239fa624dba86f74ec4858cc35aea4af8536
|
7
|
+
data.tar.gz: e63f51557b5f2431f63c920989a1b13f7ed5c1cbe5cd708091fc8f728e649caae11a6c09a754d6522b2dbbb3408cb380a2ded2f438770e8b9c2d49b782bdcd7f
|
data/README.md
CHANGED
@@ -44,7 +44,8 @@ It's designed for development, debugging, and database analysis, offering a clea
|
|
44
44
|
<details>
|
45
45
|
<summary>Click to see more screenshots</summary>
|
46
46
|
|
47
|
-
|
47
|
+
#### Dashboard Overview
|
48
|
+
|
48
49
|
<img width="1470" alt="image" src="https://github.com/user-attachments/assets/4e803d51-9a5b-4c80-bb4c-a761dba15a40" />
|
49
50
|
|
50
51
|
#### Table Details
|
@@ -76,7 +77,7 @@ gem "dbviewer", group: :development
|
|
76
77
|
And then execute:
|
77
78
|
|
78
79
|
```bash
|
79
|
-
|
80
|
+
bundle
|
80
81
|
```
|
81
82
|
|
82
83
|
## 🔧 Usage
|
@@ -274,6 +275,7 @@ With the addition of Basic Authentication, DBViewer can now be used in any envir
|
|
274
275
|
```
|
275
276
|
|
276
277
|
3. Access the tool through your regular application URL:
|
278
|
+
|
277
279
|
```
|
278
280
|
https://yourdomain.com/dbviewer?override_env_check=your_secure_random_key
|
279
281
|
```
|
@@ -288,6 +290,36 @@ When used in production, ensure:
|
|
288
290
|
- You access DBViewer over HTTPS connections only
|
289
291
|
- Access is limited to trusted administrators only
|
290
292
|
|
293
|
+
## 🔄 Updating DBViewer
|
294
|
+
|
295
|
+
To keep DBViewer up to date with the latest features, security patches, and bug fixes, follow these steps:
|
296
|
+
|
297
|
+
### Using Bundler
|
298
|
+
|
299
|
+
The simplest way to update is using Bundler:
|
300
|
+
|
301
|
+
- Update your Gemfile with the desired version:
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
# For the latest version
|
305
|
+
gem "dbviewer", group: :development
|
306
|
+
|
307
|
+
# Or specify a version
|
308
|
+
gem "dbviewer", "~> 0.3.2", group: :development
|
309
|
+
```
|
310
|
+
|
311
|
+
- Run bundle update:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
bundle update dbviewer
|
315
|
+
```
|
316
|
+
|
317
|
+
- Restart your Rails server to apply the changes:
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
rails server
|
321
|
+
```
|
322
|
+
|
291
323
|
## 🤌🏻 Contributing
|
292
324
|
|
293
325
|
Bug reports and pull requests are welcome.
|
@@ -11,6 +11,12 @@ module Dbviewer
|
|
11
11
|
@database_manager ||= ::Dbviewer::DatabaseManager.new
|
12
12
|
end
|
13
13
|
|
14
|
+
# Initialize the table query operations manager
|
15
|
+
# This gives direct access to table query operations when needed
|
16
|
+
def table_query_operations
|
17
|
+
@table_query_operations ||= database_manager.table_query_operations
|
18
|
+
end
|
19
|
+
|
14
20
|
# Get the name of the current database
|
15
21
|
def get_database_name
|
16
22
|
adapter = database_manager.connection.adapter_name.downcase
|
@@ -65,6 +71,19 @@ module Dbviewer
|
|
65
71
|
empty_tables: tables.select { |t| t[:record_count] == 0 }
|
66
72
|
}
|
67
73
|
|
74
|
+
# Calculate total foreign key relationships
|
75
|
+
begin
|
76
|
+
total_relationships = 0
|
77
|
+
tables.each do |table|
|
78
|
+
metadata = fetch_table_metadata(table[:name])
|
79
|
+
total_relationships += metadata[:foreign_keys].size if metadata && metadata[:foreign_keys]
|
80
|
+
end
|
81
|
+
analytics[:total_relationships] = total_relationships
|
82
|
+
rescue => e
|
83
|
+
Rails.logger.error("Error calculating relationship count: #{e.message}")
|
84
|
+
analytics[:total_relationships] = 0
|
85
|
+
end
|
86
|
+
|
68
87
|
# Calculate schema size if possible
|
69
88
|
begin
|
70
89
|
analytics[:schema_size] = calculate_schema_size
|
@@ -138,19 +157,8 @@ module Dbviewer
|
|
138
157
|
end
|
139
158
|
|
140
159
|
# Fetch records for a table with pagination and sorting
|
141
|
-
def fetch_table_records(table_name)
|
142
|
-
|
143
|
-
# Clean up blank filters
|
144
|
-
column_filters.reject! { |_, v| v.blank? }
|
145
|
-
|
146
|
-
database_manager.table_records(
|
147
|
-
table_name,
|
148
|
-
@current_page,
|
149
|
-
@order_by,
|
150
|
-
@order_direction,
|
151
|
-
@per_page,
|
152
|
-
column_filters || {}
|
153
|
-
)
|
160
|
+
def fetch_table_records(table_name, query_params)
|
161
|
+
database_manager.table_records(table_name, query_params)
|
154
162
|
end
|
155
163
|
|
156
164
|
# Get filtered record count for a table
|
@@ -207,6 +215,130 @@ module Dbviewer
|
|
207
215
|
relationships
|
208
216
|
end
|
209
217
|
|
218
|
+
# Get mini ERD data for a specific table and its relationships
|
219
|
+
def fetch_mini_erd_for_table(table_name)
|
220
|
+
related_tables = []
|
221
|
+
relationships = []
|
222
|
+
|
223
|
+
# Validate the table exists
|
224
|
+
unless database_manager.tables.include?(table_name)
|
225
|
+
Rails.logger.error("[DBViewer] Table not found for mini ERD: #{table_name}")
|
226
|
+
return {
|
227
|
+
tables: [],
|
228
|
+
relationships: [],
|
229
|
+
error: "Table '#{table_name}' not found in the database"
|
230
|
+
}
|
231
|
+
end
|
232
|
+
|
233
|
+
# Add current table
|
234
|
+
related_tables << { name: table_name }
|
235
|
+
|
236
|
+
Rails.logger.info("[DBViewer] Generating mini ERD for table: #{table_name}")
|
237
|
+
|
238
|
+
# Get foreign keys from this table to others (outgoing relationships)
|
239
|
+
begin
|
240
|
+
metadata = fetch_table_metadata(table_name)
|
241
|
+
Rails.logger.debug("[DBViewer] Table metadata: #{metadata.inspect}")
|
242
|
+
|
243
|
+
if metadata && metadata[:foreign_keys].present?
|
244
|
+
metadata[:foreign_keys].each do |fk|
|
245
|
+
# Ensure all required fields are present
|
246
|
+
next unless fk[:to_table].present? && fk[:column].present?
|
247
|
+
|
248
|
+
# Sanitize table and column names for display
|
249
|
+
from_table = table_name.to_s
|
250
|
+
to_table = fk[:to_table].to_s
|
251
|
+
from_column = fk[:column].to_s
|
252
|
+
to_column = fk[:primary_key].to_s.presence || "id"
|
253
|
+
relationship_name = fk[:name].to_s.presence || "#{from_table}_to_#{to_table}"
|
254
|
+
|
255
|
+
relationship = {
|
256
|
+
from_table: from_table,
|
257
|
+
to_table: to_table,
|
258
|
+
from_column: from_column,
|
259
|
+
to_column: to_column,
|
260
|
+
name: relationship_name,
|
261
|
+
direction: "outgoing"
|
262
|
+
}
|
263
|
+
|
264
|
+
relationships << relationship
|
265
|
+
Rails.logger.debug("[DBViewer] Added outgoing relationship: #{relationship.inspect}")
|
266
|
+
|
267
|
+
# Add the related table if not already included
|
268
|
+
unless related_tables.any? { |t| t[:name] == to_table }
|
269
|
+
related_tables << { name: to_table }
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
rescue => e
|
274
|
+
Rails.logger.error("[DBViewer] Error fetching outgoing relationships for #{table_name}: #{e.message}")
|
275
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
276
|
+
end
|
277
|
+
|
278
|
+
# Get foreign keys from other tables to this one (incoming relationships)
|
279
|
+
begin
|
280
|
+
database_manager.tables.each do |other_table_name|
|
281
|
+
next if other_table_name == table_name # Skip self
|
282
|
+
|
283
|
+
begin
|
284
|
+
other_metadata = fetch_table_metadata(other_table_name)
|
285
|
+
if other_metadata && other_metadata[:foreign_keys].present?
|
286
|
+
other_metadata[:foreign_keys].each do |fk|
|
287
|
+
if fk[:to_table] == table_name
|
288
|
+
# Ensure all required fields are present
|
289
|
+
next unless fk[:column].present?
|
290
|
+
|
291
|
+
# Sanitize table and column names for display
|
292
|
+
from_table = other_table_name.to_s
|
293
|
+
to_table = table_name.to_s
|
294
|
+
from_column = fk[:column].to_s
|
295
|
+
to_column = fk[:primary_key].to_s.presence || "id"
|
296
|
+
relationship_name = fk[:name].to_s.presence || "#{from_table}_to_#{to_table}"
|
297
|
+
|
298
|
+
relationship = {
|
299
|
+
from_table: from_table,
|
300
|
+
to_table: to_table,
|
301
|
+
from_column: from_column,
|
302
|
+
to_column: to_column,
|
303
|
+
name: relationship_name,
|
304
|
+
direction: "incoming"
|
305
|
+
}
|
306
|
+
|
307
|
+
relationships << relationship
|
308
|
+
Rails.logger.debug("[DBViewer] Added incoming relationship: #{relationship.inspect}")
|
309
|
+
|
310
|
+
# Add the related table if not already included
|
311
|
+
unless related_tables.any? { |t| t[:name] == from_table }
|
312
|
+
related_tables << { name: from_table }
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
rescue => e
|
318
|
+
Rails.logger.error("[DBViewer] Error processing relationships for table #{other_table_name}: #{e.message}")
|
319
|
+
# Continue to the next table
|
320
|
+
end
|
321
|
+
end
|
322
|
+
rescue => e
|
323
|
+
Rails.logger.error("[DBViewer] Error fetching incoming relationships for #{table_name}: #{e.message}")
|
324
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
325
|
+
end
|
326
|
+
|
327
|
+
# If no relationships were found, make sure to still include at least the current table
|
328
|
+
if relationships.empty?
|
329
|
+
Rails.logger.info("[DBViewer] No relationships found for table: #{table_name}")
|
330
|
+
end
|
331
|
+
|
332
|
+
result = {
|
333
|
+
tables: related_tables,
|
334
|
+
relationships: relationships,
|
335
|
+
timestamp: Time.now.to_i
|
336
|
+
}
|
337
|
+
|
338
|
+
Rails.logger.info("[DBViewer] Mini ERD data generated: #{related_tables.length} tables, #{relationships.length} relationships")
|
339
|
+
result
|
340
|
+
end
|
341
|
+
|
210
342
|
# Prepare the SQL query - either from params or default
|
211
343
|
def prepare_query
|
212
344
|
quoted_table = safe_quote_table_name(@table_name)
|
@@ -240,17 +372,24 @@ module Dbviewer
|
|
240
372
|
end
|
241
373
|
|
242
374
|
# Export table data to CSV
|
243
|
-
def export_table_to_csv(table_name,
|
375
|
+
def export_table_to_csv(table_name, query_params = nil, include_headers = true)
|
244
376
|
require "csv"
|
245
377
|
|
246
378
|
begin
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
limit
|
253
|
-
|
379
|
+
if query_params.is_a?(Dbviewer::TableQueryParams)
|
380
|
+
# Use the query params object directly
|
381
|
+
records = database_manager.query_operations.table_records(table_name, query_params)
|
382
|
+
else
|
383
|
+
# Legacy support for the old method signature
|
384
|
+
limit = query_params.is_a?(Numeric) ? query_params : 10000
|
385
|
+
records = database_manager.table_records(
|
386
|
+
table_name,
|
387
|
+
1, # First page
|
388
|
+
nil, # Default sorting
|
389
|
+
"asc",
|
390
|
+
limit # Limit number of records
|
391
|
+
)
|
392
|
+
end
|
254
393
|
|
255
394
|
csv_data = CSV.generate do |csv|
|
256
395
|
# Add headers if requested
|
@@ -35,6 +35,14 @@ module Dbviewer
|
|
35
35
|
@order_direction = "ASC" unless self.class::VALID_SORT_DIRECTIONS.include?(@order_direction)
|
36
36
|
end
|
37
37
|
|
38
|
+
def fetch_total_count(table_name, query_params)
|
39
|
+
if query_params.column_filters.present? && query_params.column_filters.values.any?(&:present?)
|
40
|
+
fetch_filtered_record_count(table_name, query_params.column_filters)
|
41
|
+
else
|
42
|
+
fetch_table_record_count(table_name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
38
46
|
# Calculate the total number of pages
|
39
47
|
def calculate_total_pages(total_count, per_page)
|
40
48
|
(total_count.to_f / per_page).ceil
|
@@ -2,30 +2,40 @@ module Dbviewer
|
|
2
2
|
class TablesController < ApplicationController
|
3
3
|
include Dbviewer::PaginationConcern
|
4
4
|
|
5
|
+
before_action :set_table_name, except: [ :index ]
|
6
|
+
|
5
7
|
def index
|
6
8
|
@tables = fetch_tables_with_stats(include_record_counts: true)
|
7
9
|
end
|
8
10
|
|
9
11
|
def show
|
10
|
-
@table_name = params[:id]
|
11
|
-
@columns = fetch_table_columns(@table_name)
|
12
|
-
@metadata = fetch_table_metadata(@table_name)
|
13
|
-
@tables = fetch_tables_with_stats # Fetch tables for sidebar
|
14
|
-
|
15
12
|
set_pagination_params
|
16
13
|
set_sorting_params
|
17
14
|
|
18
|
-
#
|
15
|
+
# Get column filters from params first
|
19
16
|
@column_filters = params[:column_filters].presence ? params[:column_filters].to_enum.to_h : {}
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
# Then apply global creation filters (this will modify @column_filters)
|
19
|
+
set_global_filters
|
20
|
+
|
21
|
+
# Now create the query params with the combined filters
|
22
|
+
query_params = Dbviewer::TableQueryParams.new(
|
23
|
+
page: @current_page,
|
24
|
+
per_page: @per_page,
|
25
|
+
order_by: @order_by,
|
26
|
+
direction: @order_direction,
|
27
|
+
column_filters: @column_filters.reject { |_, v| v.blank? }
|
28
|
+
)
|
29
|
+
@total_count = fetch_total_count(@table_name, query_params)
|
30
|
+
@records = fetch_table_records(@table_name, query_params)
|
27
31
|
@total_pages = calculate_total_pages(@total_count, @per_page)
|
28
|
-
@
|
32
|
+
@columns = fetch_table_columns(@table_name)
|
33
|
+
@metadata = fetch_table_metadata(@table_name)
|
34
|
+
|
35
|
+
if @records.nil?
|
36
|
+
column_names = @columns.map { |c| c[:name] }
|
37
|
+
@records = ActiveRecord::Result.new(column_names, [])
|
38
|
+
end
|
29
39
|
|
30
40
|
# Fetch timestamp visualization data if the table has a created_at column
|
31
41
|
if has_timestamp_column?(@table_name)
|
@@ -46,8 +56,33 @@ module Dbviewer
|
|
46
56
|
end
|
47
57
|
end
|
48
58
|
|
59
|
+
def mini_erd
|
60
|
+
begin
|
61
|
+
@erd_data = fetch_mini_erd_for_table(@table_name)
|
62
|
+
|
63
|
+
if @erd_data[:error].present?
|
64
|
+
Rails.logger.error("Mini ERD error: #{@erd_data[:error]}")
|
65
|
+
end
|
66
|
+
|
67
|
+
respond_to do |format|
|
68
|
+
format.json { render json: @erd_data }
|
69
|
+
format.html { render layout: false }
|
70
|
+
end
|
71
|
+
rescue => e
|
72
|
+
Rails.logger.error("Error generating Mini ERD: #{e.message}")
|
73
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
74
|
+
|
75
|
+
@error_message = e.message
|
76
|
+
@erd_data = { tables: [], relationships: [], error: @error_message }
|
77
|
+
|
78
|
+
respond_to do |format|
|
79
|
+
format.json { render json: { error: @error_message }, status: :internal_server_error }
|
80
|
+
format.html { render layout: false }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
49
85
|
def query
|
50
|
-
@table_name = params[:id]
|
51
86
|
@read_only_mode = true # Flag to indicate we're in read-only mode
|
52
87
|
@columns = fetch_table_columns(@table_name)
|
53
88
|
@tables = fetch_tables_with_stats # Fetch tables for sidebar
|
@@ -68,20 +103,98 @@ module Dbviewer
|
|
68
103
|
return
|
69
104
|
end
|
70
105
|
|
71
|
-
table_name = params[:id]
|
72
106
|
limit = (params[:limit] || 10000).to_i
|
73
107
|
include_headers = params[:include_headers] != "0"
|
74
108
|
|
75
|
-
|
109
|
+
# Apply global creation filters
|
110
|
+
set_global_filters
|
111
|
+
|
112
|
+
# Create query parameters for export
|
113
|
+
query_params = Dbviewer::TableQueryParams.new(
|
114
|
+
page: 1,
|
115
|
+
per_page: limit,
|
116
|
+
order_by: nil,
|
117
|
+
direction: "asc",
|
118
|
+
column_filters: @column_filters.reject { |_, v| v.blank? }
|
119
|
+
)
|
120
|
+
|
121
|
+
# Get filtered data for export
|
122
|
+
csv_data = export_table_to_csv(@table_name, query_params, include_headers)
|
76
123
|
|
77
124
|
# Set filename with timestamp for uniqueness
|
78
125
|
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
79
|
-
|
126
|
+
|
127
|
+
# Add filter info to filename if filters are applied
|
128
|
+
filename_suffix = ""
|
129
|
+
if @creation_filter_start.present? || @creation_filter_end.present?
|
130
|
+
filename_suffix = "_filtered"
|
131
|
+
end
|
132
|
+
|
133
|
+
filename = "#{@table_name}#{filename_suffix}_#{timestamp}.csv"
|
80
134
|
|
81
135
|
# Send data as file
|
82
136
|
send_data csv_data,
|
83
137
|
type: "text/csv; charset=utf-8; header=present",
|
84
138
|
disposition: "attachment; filename=#{filename}"
|
85
139
|
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def set_table_name
|
144
|
+
@table_name = params[:id]
|
145
|
+
end # Handle global creation datetime filters across tables
|
146
|
+
def set_global_filters
|
147
|
+
# Store creation filter datetimes in session to persist between table navigation
|
148
|
+
if params[:creation_filter_start].present?
|
149
|
+
session[:creation_filter_start] = params[:creation_filter_start]
|
150
|
+
end
|
151
|
+
|
152
|
+
if params[:creation_filter_end].present?
|
153
|
+
session[:creation_filter_end] = params[:creation_filter_end]
|
154
|
+
end
|
155
|
+
|
156
|
+
# Clear filters if explicitly requested
|
157
|
+
if params[:clear_creation_filter] == "true"
|
158
|
+
session.delete(:creation_filter_start)
|
159
|
+
session.delete(:creation_filter_end)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Set instance variables for view access
|
163
|
+
@creation_filter_start = session[:creation_filter_start]
|
164
|
+
@creation_filter_end = session[:creation_filter_end]
|
165
|
+
|
166
|
+
# Initialize column_filters if not present
|
167
|
+
@column_filters ||= {}
|
168
|
+
|
169
|
+
# Apply creation filters to column_filters if the table has a created_at column
|
170
|
+
if has_timestamp_column?(@table_name) && (@creation_filter_start.present? || @creation_filter_end.present?)
|
171
|
+
# Clear any existing created_at filters to avoid conflicts
|
172
|
+
@column_filters.delete("created_at")
|
173
|
+
@column_filters.delete("created_at_operator")
|
174
|
+
@column_filters.delete("created_at_end")
|
175
|
+
|
176
|
+
if @creation_filter_start.present? && @creation_filter_end.present?
|
177
|
+
# If both start and end are present, set up a range filter
|
178
|
+
@column_filters["created_at"] = @creation_filter_start
|
179
|
+
@column_filters["created_at_end"] = @creation_filter_end
|
180
|
+
# No need for operator when using start and end together
|
181
|
+
Rails.logger.info("[DBViewer] Setting creation filter range: #{@creation_filter_start} to #{@creation_filter_end}")
|
182
|
+
elsif @creation_filter_start.present?
|
183
|
+
# Only start date is present
|
184
|
+
@column_filters["created_at"] = @creation_filter_start
|
185
|
+
@column_filters["created_at_operator"] = "gte" # Greater than or equal
|
186
|
+
Rails.logger.info("[DBViewer] Setting creation filter start: #{@creation_filter_start}")
|
187
|
+
elsif @creation_filter_end.present?
|
188
|
+
# Only end date is present
|
189
|
+
@column_filters["created_at"] = @creation_filter_end
|
190
|
+
@column_filters["created_at_operator"] = "lte" # Less than or equal
|
191
|
+
Rails.logger.info("[DBViewer] Setting creation filter end: #{@creation_filter_end}")
|
192
|
+
end
|
193
|
+
else
|
194
|
+
if @creation_filter_start.present? || @creation_filter_end.present?
|
195
|
+
Rails.logger.info("[DBViewer] Creation filter present but table #{@table_name} has no created_at column")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
86
199
|
end
|
87
200
|
end
|
@@ -1,5 +1,19 @@
|
|
1
1
|
module Dbviewer
|
2
2
|
module ApplicationHelper
|
3
|
+
# Check if a table has a created_at column
|
4
|
+
def has_timestamp_column?(table_name)
|
5
|
+
return false unless table_name.present?
|
6
|
+
|
7
|
+
# Get the columns for the table directly using DatabaseManager
|
8
|
+
columns = get_database_manager.table_columns(table_name)
|
9
|
+
columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Helper to access the database manager
|
13
|
+
def get_database_manager
|
14
|
+
@database_manager ||= ::Dbviewer::DatabaseManager.new
|
15
|
+
end
|
16
|
+
|
3
17
|
def format_cell_value(value)
|
4
18
|
return "NULL" if value.nil?
|
5
19
|
return value.to_s.truncate(100) unless value.is_a?(String)
|
@@ -114,5 +128,64 @@ module Dbviewer
|
|
114
128
|
def logs_nav_class
|
115
129
|
active_nav_class("logs")
|
116
130
|
end
|
131
|
+
|
132
|
+
# Returns a sort icon based on the current sort direction
|
133
|
+
def sort_icon(column_name, current_order_by, current_direction)
|
134
|
+
if column_name == current_order_by
|
135
|
+
direction = current_direction == "ASC" ? "up" : "down"
|
136
|
+
"<i class='bi bi-sort-#{direction}'></i>".html_safe
|
137
|
+
else
|
138
|
+
"<i class='bi bi-filter invisible sort-icon'></i>".html_safe
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Determine the next sort direction based on the current one
|
143
|
+
def next_sort_direction(column_name, current_order_by, current_direction)
|
144
|
+
if column_name == current_order_by && current_direction == "ASC"
|
145
|
+
"DESC"
|
146
|
+
else
|
147
|
+
"ASC"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Generate a sortable column header link
|
152
|
+
def sortable_column_header(column_name, current_order_by, current_direction, table_name, current_page, per_page, column_filters)
|
153
|
+
is_sorted = column_name == current_order_by
|
154
|
+
sort_direction = next_sort_direction(column_name, current_order_by, current_direction)
|
155
|
+
|
156
|
+
aria_sort = if is_sorted
|
157
|
+
current_direction.downcase == "asc" ? "ascending" : "descending"
|
158
|
+
else
|
159
|
+
"none"
|
160
|
+
end
|
161
|
+
|
162
|
+
# Build parameters for the sort link
|
163
|
+
sort_params = {
|
164
|
+
order_by: column_name,
|
165
|
+
order_direction: sort_direction,
|
166
|
+
page: current_page,
|
167
|
+
per_page: per_page,
|
168
|
+
column_filters: column_filters
|
169
|
+
}
|
170
|
+
|
171
|
+
# Add creation filter parameters if they're in the controller
|
172
|
+
if defined?(@creation_filter_start) && @creation_filter_start.present?
|
173
|
+
sort_params[:creation_filter_start] = @creation_filter_start
|
174
|
+
end
|
175
|
+
|
176
|
+
if defined?(@creation_filter_end) && @creation_filter_end.present?
|
177
|
+
sort_params[:creation_filter_end] = @creation_filter_end
|
178
|
+
end
|
179
|
+
|
180
|
+
link_to table_path(table_name, sort_params),
|
181
|
+
class: "d-flex align-items-center text-decoration-none text-reset column-sort-link",
|
182
|
+
title: "Sort by #{column_name} (#{sort_direction.downcase})",
|
183
|
+
"aria-sort": aria_sort,
|
184
|
+
role: "button",
|
185
|
+
tabindex: "0" do
|
186
|
+
content_tag(:span, column_name, class: "column-name") +
|
187
|
+
content_tag(:span, sort_icon(column_name, current_order_by, current_direction), class: "sort-icon-container")
|
188
|
+
end
|
189
|
+
end
|
117
190
|
end
|
118
191
|
end
|
@@ -15,37 +15,58 @@
|
|
15
15
|
|
16
16
|
<div class="row g-3 mb-4">
|
17
17
|
<div class="col-md-3">
|
18
|
-
<div class="card h-100
|
19
|
-
<div class="card-body">
|
20
|
-
<
|
21
|
-
|
18
|
+
<div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
|
19
|
+
<div class="card-body d-flex align-items-center">
|
20
|
+
<div class="metric-icon me-3">
|
21
|
+
<i class="bi bi-table fs-4"></i>
|
22
|
+
</div>
|
23
|
+
<div class="text-start">
|
24
|
+
<h5 class="mb-1">Tables</h5>
|
25
|
+
<h2 class="mb-0"><%= @analytics[:total_tables] %></h2>
|
26
|
+
</div>
|
22
27
|
</div>
|
23
28
|
</div>
|
24
29
|
</div>
|
25
30
|
|
26
31
|
<div class="col-md-3">
|
27
|
-
<div class="card h-100
|
28
|
-
<div class="card-body">
|
29
|
-
<
|
30
|
-
|
32
|
+
<div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
|
33
|
+
<div class="card-body d-flex align-items-center">
|
34
|
+
<div class="metric-icon me-3">
|
35
|
+
<i class="bi bi-database fs-4"></i>
|
36
|
+
</div>
|
37
|
+
<div class="text-start">
|
38
|
+
<h5 class="mb-1">Records</h5>
|
39
|
+
<h2 class="mb-0"><%= number_with_delimiter(@analytics[:total_records]) %></h2>
|
40
|
+
</div>
|
31
41
|
</div>
|
32
42
|
</div>
|
33
43
|
</div>
|
34
44
|
|
35
45
|
<div class="col-md-3">
|
36
|
-
<div class="card h-100
|
37
|
-
<div class="card-body">
|
38
|
-
<
|
39
|
-
|
46
|
+
<div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
|
47
|
+
<div class="card-body d-flex align-items-center">
|
48
|
+
<div class="metric-icon me-3">
|
49
|
+
<i class="bi bi-link-45deg fs-4"></i>
|
50
|
+
</div>
|
51
|
+
<div class="text-start">
|
52
|
+
<h5 class="mb-1">Relationships</h5>
|
53
|
+
<h2 class="mb-0"><%= @analytics[:total_relationships] %></h2>
|
54
|
+
<small class="text-muted d-block">Foreign Key Connections</small>
|
55
|
+
</div>
|
40
56
|
</div>
|
41
57
|
</div>
|
42
58
|
</div>
|
43
59
|
|
44
60
|
<div class="col-md-3">
|
45
|
-
<div class="card h-100
|
46
|
-
<div class="card-body">
|
47
|
-
<
|
48
|
-
|
61
|
+
<div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
|
62
|
+
<div class="card-body d-flex align-items-center">
|
63
|
+
<div class="metric-icon me-3">
|
64
|
+
<i class="bi bi-hdd fs-4"></i>
|
65
|
+
</div>
|
66
|
+
<div class="text-start">
|
67
|
+
<h5 class="mb-1">Database Size</h5>
|
68
|
+
<h2 class="mb-0"><%= number_to_human_size(@analytics[:schema_size]) %></h2>
|
69
|
+
</div>
|
49
70
|
</div>
|
50
71
|
</div>
|
51
72
|
</div>
|