dbviewer 0.7.10 → 0.8.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 +4 -4
- data/README.md +60 -0
- data/app/assets/images/dbviewer/emoji-favicon.txt +1 -0
- data/app/assets/images/dbviewer/favicon.ico +4 -0
- data/app/assets/images/dbviewer/favicon.png +4 -0
- data/app/assets/images/dbviewer/favicon.svg +10 -0
- data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +38 -42
- data/app/assets/javascripts/dbviewer/error_handler.js +58 -0
- data/app/assets/javascripts/dbviewer/home.js +25 -34
- data/app/assets/javascripts/dbviewer/layout.js +100 -129
- data/app/assets/javascripts/dbviewer/query.js +309 -246
- data/app/assets/javascripts/dbviewer/sidebar.js +170 -183
- data/app/assets/javascripts/dbviewer/utility.js +124 -0
- data/app/assets/stylesheets/dbviewer/application.css +8 -146
- data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +0 -34
- data/app/assets/stylesheets/dbviewer/logs.css +0 -11
- data/app/assets/stylesheets/dbviewer/query.css +21 -9
- data/app/assets/stylesheets/dbviewer/table.css +49 -131
- data/app/controllers/concerns/dbviewer/database_operations/connection_management.rb +90 -0
- data/app/controllers/concerns/dbviewer/database_operations/data_export.rb +31 -0
- data/app/controllers/concerns/dbviewer/database_operations/database_information.rb +54 -0
- data/app/controllers/concerns/dbviewer/database_operations/datatable_operations.rb +37 -0
- data/app/controllers/concerns/dbviewer/database_operations/query_operations.rb +37 -0
- data/app/controllers/concerns/dbviewer/database_operations/relationship_management.rb +175 -0
- data/app/controllers/concerns/dbviewer/database_operations/table_operations.rb +46 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +4 -9
- data/app/controllers/dbviewer/api/tables_controller.rb +12 -0
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +0 -15
- data/app/controllers/dbviewer/tables_controller.rb +4 -33
- data/app/helpers/dbviewer/datatable_ui_table_helper.rb +10 -9
- data/app/helpers/dbviewer/formatting_helper.rb +6 -1
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +1 -1
- data/app/views/dbviewer/tables/query.html.erb +24 -8
- data/app/views/dbviewer/tables/show.html.erb +3 -3
- data/app/views/layouts/dbviewer/application.html.erb +12 -3
- data/config/routes.rb +2 -2
- data/lib/dbviewer/configuration.rb +21 -0
- data/lib/dbviewer/data_privacy/pii_masker.rb +125 -0
- data/lib/dbviewer/database/manager.rb +2 -2
- data/lib/dbviewer/datatable/query_operations.rb +1 -17
- data/lib/dbviewer/engine.rb +29 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +45 -0
- data/lib/generators/dbviewer/install_generator.rb +6 -0
- data/lib/generators/dbviewer/templates/pii_configuration_example.rb +99 -0
- metadata +17 -10
- data/app/controllers/concerns/dbviewer/connection_management.rb +0 -88
- data/app/controllers/concerns/dbviewer/data_export.rb +0 -32
- data/app/controllers/concerns/dbviewer/database_information.rb +0 -62
- data/app/controllers/concerns/dbviewer/datatable_support.rb +0 -47
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +0 -34
- data/app/controllers/concerns/dbviewer/query_operations.rb +0 -28
- data/app/controllers/concerns/dbviewer/relationship_management.rb +0 -173
- data/app/controllers/concerns/dbviewer/table_operations.rb +0 -56
@@ -0,0 +1,31 @@
|
|
1
|
+
require "csv"
|
2
|
+
|
3
|
+
module Dbviewer
|
4
|
+
module DatabaseOperations
|
5
|
+
module DataExport
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
# Export table data to CSV
|
9
|
+
def export_table_to_csv(table_name, query_params = nil, include_headers = true)
|
10
|
+
records = database_manager.table_query_operations.table_records(table_name, query_params)
|
11
|
+
|
12
|
+
CSV.generate do |csv|
|
13
|
+
csv << records.columns if include_headers
|
14
|
+
|
15
|
+
record_body = records.rows.map do |row|
|
16
|
+
row.map { |cell| format_csv_value(cell) }
|
17
|
+
end
|
18
|
+
csv.concat(record_body)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Format cell values for CSV export to handle nil values and special characters
|
25
|
+
def format_csv_value(value)
|
26
|
+
return "" if value.nil?
|
27
|
+
value.to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module DatabaseOperations
|
3
|
+
module DatabaseInformation
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
helper_method :get_database_name, :get_adapter_name if respond_to?(:helper_method)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Get the name of the current database
|
11
|
+
def get_database_name
|
12
|
+
current_conn_config = Dbviewer.configuration.database_connections[current_connection_key]
|
13
|
+
return current_conn_config[:name] if current_conn_config && current_conn_config[:name].present?
|
14
|
+
|
15
|
+
adapter = database_manager.connection.adapter_name.downcase
|
16
|
+
fetch_database_name(adapter)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get the name of the current database adapter
|
20
|
+
def get_adapter_name
|
21
|
+
adapter_name = database_manager.connection.adapter_name.downcase
|
22
|
+
adapter_mappings = {
|
23
|
+
/mysql/i => "MySQL",
|
24
|
+
/postgres/i => "PostgreSQL",
|
25
|
+
/sqlite/i => "SQLite",
|
26
|
+
/oracle/i => "Oracle",
|
27
|
+
/sqlserver|mssql/i => "SQL Server"
|
28
|
+
}
|
29
|
+
adapter_mappings.find { |pattern, _| adapter_name =~ pattern }&.last || adapter_name.titleize
|
30
|
+
rescue
|
31
|
+
"Unknown"
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch_database_name(adapter)
|
35
|
+
case adapter
|
36
|
+
when /mysql/
|
37
|
+
query = "SELECT DATABASE() as db_name"
|
38
|
+
result = database_manager.execute_query(query).first
|
39
|
+
result ? result["db_name"] : "Database"
|
40
|
+
when /postgres/
|
41
|
+
query = "SELECT current_database() as db_name"
|
42
|
+
result = database_manager.execute_query(query).first
|
43
|
+
result ? result["db_name"] : "Database"
|
44
|
+
when /sqlite/
|
45
|
+
# For SQLite, extract the database name from the connection_config
|
46
|
+
database_path = database_manager.connection.pool.spec.config[:database] || ""
|
47
|
+
File.basename(database_path, ".*") || "SQLite Database"
|
48
|
+
else
|
49
|
+
"Database" # Default fallback
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module DatabaseOperations
|
3
|
+
module DatatableOperations
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Consolidated method to fetch all datatable-related data in one call
|
7
|
+
# Returns a hash containing all necessary datatable information
|
8
|
+
def fetch_datatable_data(table_name, query_params)
|
9
|
+
columns = fetch_table_columns(table_name)
|
10
|
+
|
11
|
+
total_count = fetch_table_record_count(table_name, query_params.column_filters)
|
12
|
+
records = fetch_table_records(table_name, query_params)
|
13
|
+
metadata = fetch_table_metadata(table_name)
|
14
|
+
|
15
|
+
{
|
16
|
+
columns: columns,
|
17
|
+
records: records,
|
18
|
+
total_count: total_count,
|
19
|
+
total_pages: total_count > 0 ? (total_count.to_f / query_params.per_page).ceil : 0,
|
20
|
+
metadata: metadata,
|
21
|
+
current_page: query_params.page,
|
22
|
+
per_page: query_params.per_page,
|
23
|
+
order_by: query_params.order_by,
|
24
|
+
direction: query_params.direction
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def fetch_table_stats(table_name)
|
29
|
+
{
|
30
|
+
table_name: table_name,
|
31
|
+
columns: fetch_table_columns(table_name),
|
32
|
+
metadata: fetch_table_metadata(table_name)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module DatabaseOperations
|
3
|
+
module QueryOperations
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Prepare the SQL query - either from params or default
|
7
|
+
def prepare_query(table_name, query)
|
8
|
+
query = query.present? ? query.to_s : default_query(table_name)
|
9
|
+
|
10
|
+
# Validate query for security
|
11
|
+
unless ::Dbviewer::Validator::Sql.safe_query?(query)
|
12
|
+
query = default_query(table_name)
|
13
|
+
flash.now[:warning] = "Only SELECT queries are allowed. Your query contained potentially unsafe operations. Using default query instead."
|
14
|
+
end
|
15
|
+
|
16
|
+
query
|
17
|
+
end
|
18
|
+
|
19
|
+
# Execute the prepared SQL query
|
20
|
+
def execute_query(query)
|
21
|
+
database_manager.execute_query(@query)
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_query(table_name)
|
25
|
+
quoted_table = safe_quote_table_name(table_name)
|
26
|
+
"SELECT * FROM #{quoted_table} LIMIT 100"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Safely quote a table name, with fallback
|
32
|
+
def safe_quote_table_name(table_name)
|
33
|
+
database_manager.connection.quote_table_name(table_name) rescue table_name.to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module DatabaseOperations
|
3
|
+
module RelationshipManagement
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Fetch relationships between tables for ERD visualization
|
7
|
+
def fetch_table_relationships(tables)
|
8
|
+
tables.flat_map { |table| extract_table_relationships_from_metadata(table[:name]) }
|
9
|
+
end
|
10
|
+
|
11
|
+
# Get mini ERD data for a specific table and its relationships
|
12
|
+
def fetch_mini_erd_for_table(table_name)
|
13
|
+
outgoing_data = collect_outgoing_relationships(table_name)
|
14
|
+
incoming_data = collect_incoming_relationships(table_name)
|
15
|
+
|
16
|
+
initial_tables = [ { name: table_name } ]
|
17
|
+
all_relationships = outgoing_data[:relationships] + incoming_data[:relationships]
|
18
|
+
all_tables = (initial_tables + outgoing_data[:tables] + incoming_data[:tables]).uniq { |t| t[:name] }
|
19
|
+
|
20
|
+
{
|
21
|
+
tables: all_tables,
|
22
|
+
relationships: all_relationships,
|
23
|
+
timestamp: Time.now.to_i
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Extract relationships for a single table from its metadata
|
30
|
+
# @param table_name [String] The name of the table to process
|
31
|
+
# @return [Array<Hash>] Array of relationship hashes for this table
|
32
|
+
def extract_table_relationships_from_metadata(table_name)
|
33
|
+
metadata = database_manager.table_metadata(table_name)
|
34
|
+
return [] unless metadata&.dig(:foreign_keys)&.present?
|
35
|
+
|
36
|
+
metadata[:foreign_keys].map do |fk|
|
37
|
+
{
|
38
|
+
from_table: table_name,
|
39
|
+
to_table: fk[:to_table],
|
40
|
+
from_column: fk[:column],
|
41
|
+
to_column: fk[:primary_key],
|
42
|
+
name: fk[:name]
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Collect outgoing relationships from the specified table to other tables
|
48
|
+
# @param table_name [String] The source table name
|
49
|
+
# @return [Hash] Hash containing :tables and :relationships arrays
|
50
|
+
def collect_outgoing_relationships(table_name)
|
51
|
+
tables = []
|
52
|
+
relationships = []
|
53
|
+
|
54
|
+
metadata = fetch_table_metadata(table_name)
|
55
|
+
return { tables: tables, relationships: relationships } unless metadata&.dig(:foreign_keys)&.present?
|
56
|
+
|
57
|
+
metadata[:foreign_keys].each do |fk|
|
58
|
+
result = process_outgoing_foreign_key(table_name, fk)
|
59
|
+
if result
|
60
|
+
relationships << result[:relationship]
|
61
|
+
tables << result[:table]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
{
|
66
|
+
tables: tables,
|
67
|
+
relationships: relationships
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
# Process a single outgoing foreign key relationship
|
72
|
+
# @param table_name [String] The source table name
|
73
|
+
# @param fk [Hash] Foreign key metadata
|
74
|
+
# @return [Hash, nil] Hash containing :relationship and :table, or nil if invalid
|
75
|
+
def process_outgoing_foreign_key(table_name, fk)
|
76
|
+
return nil unless fk[:to_table].present? && fk[:column].present?
|
77
|
+
|
78
|
+
relationship = build_relationship_hash(
|
79
|
+
from_table: table_name.to_s,
|
80
|
+
to_table: fk[:to_table].to_s,
|
81
|
+
from_column: fk[:column].to_s,
|
82
|
+
to_column: fk[:primary_key].to_s.presence || "id",
|
83
|
+
name: fk[:name].to_s.presence || "#{table_name}_to_#{fk[:to_table]}",
|
84
|
+
direction: "outgoing"
|
85
|
+
)
|
86
|
+
|
87
|
+
{
|
88
|
+
relationship: relationship,
|
89
|
+
table: {
|
90
|
+
name: fk[:to_table].to_s
|
91
|
+
}
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
# Collect incoming relationships from other tables to the specified table
|
96
|
+
# @param table_name [String] The target table name
|
97
|
+
# @return [Hash] Hash containing :tables and :relationships arrays
|
98
|
+
def collect_incoming_relationships(table_name)
|
99
|
+
results = database_manager.tables
|
100
|
+
.reject { |other_table_name| other_table_name == table_name }
|
101
|
+
.map { |other_table_name| process_table_for_incoming_relationships(table_name, other_table_name) }
|
102
|
+
.compact
|
103
|
+
|
104
|
+
{
|
105
|
+
tables: results.flat_map { |result| result[:tables] },
|
106
|
+
relationships: results.flat_map { |result| result[:relationships] }
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# Process a single table to find incoming relationships to the target table
|
111
|
+
# @param target_table [String] The target table name
|
112
|
+
# @param source_table [String] The source table name to check
|
113
|
+
# @return [Hash, nil] Hash containing :tables and :relationships arrays, or nil if no relationships
|
114
|
+
def process_table_for_incoming_relationships(target_table, source_table)
|
115
|
+
other_metadata = fetch_table_metadata(source_table)
|
116
|
+
return nil unless other_metadata&.dig(:foreign_keys)&.present?
|
117
|
+
|
118
|
+
results = other_metadata[:foreign_keys]
|
119
|
+
.map { |fk| process_incoming_foreign_key(target_table, source_table, fk) }
|
120
|
+
.compact
|
121
|
+
|
122
|
+
return nil if results.empty?
|
123
|
+
|
124
|
+
{
|
125
|
+
tables: results.map { |result| result[:table] },
|
126
|
+
relationships: results.map { |result| result[:relationship] }
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
# Process a single incoming foreign key relationship
|
131
|
+
# @param target_table [String] The target table name
|
132
|
+
# @param source_table [String] The source table name
|
133
|
+
# @param fk [Hash] Foreign key metadata
|
134
|
+
# @return [Hash, nil] Hash containing :relationship and :table, or nil if invalid
|
135
|
+
def process_incoming_foreign_key(target_table, source_table, fk)
|
136
|
+
return nil unless fk[:to_table] == target_table && fk[:column].present?
|
137
|
+
|
138
|
+
relationship = build_relationship_hash(
|
139
|
+
from_table: source_table.to_s,
|
140
|
+
to_table: target_table.to_s,
|
141
|
+
from_column: fk[:column].to_s,
|
142
|
+
to_column: fk[:primary_key].to_s.presence || "id",
|
143
|
+
name: fk[:name].to_s.presence || "#{source_table}_to_#{target_table}",
|
144
|
+
direction: "incoming"
|
145
|
+
)
|
146
|
+
|
147
|
+
{
|
148
|
+
relationship: relationship,
|
149
|
+
table: {
|
150
|
+
name: source_table.to_s
|
151
|
+
}
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
# Build a standardized relationship hash
|
156
|
+
# @param from_table [String] Source table name
|
157
|
+
# @param to_table [String] Target table name
|
158
|
+
# @param from_column [String] Source column name
|
159
|
+
# @param to_column [String] Target column name
|
160
|
+
# @param name [String] Relationship name
|
161
|
+
# @param direction [String] Relationship direction
|
162
|
+
# @return [Hash] Standardized relationship hash
|
163
|
+
def build_relationship_hash(from_table:, to_table:, from_column:, to_column:, name:, direction:)
|
164
|
+
{
|
165
|
+
from_table: from_table,
|
166
|
+
to_table: to_table,
|
167
|
+
from_column: from_column,
|
168
|
+
to_column: to_column,
|
169
|
+
name: name,
|
170
|
+
direction: direction
|
171
|
+
}
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module DatabaseOperations
|
3
|
+
module TableOperations
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Fetch all tables with their stats
|
7
|
+
# By default, don't include record counts for better performance on sidebar
|
8
|
+
def fetch_tables(include_record_counts = false)
|
9
|
+
database_manager.tables.map do |table_name|
|
10
|
+
table_stats = { name: table_name }
|
11
|
+
table_stats[:record_count] = fetch_table_record_count(table_name) if include_record_counts
|
12
|
+
table_stats
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get column information for a specific table
|
17
|
+
def fetch_table_columns(table_name)
|
18
|
+
database_manager.table_columns(table_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Fetch records for a table with pagination and sorting
|
22
|
+
def fetch_table_records(table_name, query_params)
|
23
|
+
database_manager.table_records(table_name, query_params)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get filtered record count for a table
|
27
|
+
def fetch_table_record_count(table_name, column_filters = {})
|
28
|
+
database_manager.table_record_count(table_name, column_filters)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get table metadata for display (e.g., primary key, foreign keys, indexes)
|
32
|
+
def fetch_table_metadata(table_name)
|
33
|
+
database_manager.table_metadata(table_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Check if a table has a created_at column for timestamp visualization
|
39
|
+
def has_timestamp_column?(table_name)
|
40
|
+
fetch_table_columns(table_name).any? do |column|
|
41
|
+
column[:name] == "created_at" && [ :datetime, :timestamp ].include?(column[:type])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -3,22 +3,17 @@ module Dbviewer
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
include ConnectionManagement
|
6
|
+
include DataExport
|
6
7
|
include DatabaseInformation
|
7
|
-
include
|
8
|
-
include RelationshipManagement
|
8
|
+
include DatatableOperations
|
9
9
|
include QueryOperations
|
10
|
-
include
|
11
|
-
include
|
12
|
-
|
13
|
-
# -- Database Managers --
|
10
|
+
include RelationshipManagement
|
11
|
+
include TableOperations
|
14
12
|
|
15
|
-
# Initialize the database manager with the current connection
|
16
13
|
def database_manager
|
17
14
|
@database_manager = ::Dbviewer::Database::Manager.new(current_connection_key)
|
18
15
|
end
|
19
16
|
|
20
|
-
# Initialize the table query operations manager
|
21
|
-
# This gives direct access to table query operations when needed
|
22
17
|
def table_query_operations
|
23
18
|
@table_query_operations ||= database_manager.table_query_operations
|
24
19
|
end
|
@@ -6,6 +6,11 @@ module Dbviewer
|
|
6
6
|
render_success(total_tables: tables_count)
|
7
7
|
end
|
8
8
|
|
9
|
+
def show
|
10
|
+
table_stats = fetch_table_stats(params[:id])
|
11
|
+
render_success(**table_stats)
|
12
|
+
end
|
13
|
+
|
9
14
|
def records
|
10
15
|
tables_stats = fetch_tables_stats
|
11
16
|
render_success(tables_stats)
|
@@ -36,6 +41,13 @@ module Dbviewer
|
|
36
41
|
})
|
37
42
|
end
|
38
43
|
|
44
|
+
def mini_erd
|
45
|
+
table_name = params[:id]
|
46
|
+
erd_data = fetch_mini_erd_for_table(table_name)
|
47
|
+
|
48
|
+
render_success(erd_data)
|
49
|
+
end
|
50
|
+
|
39
51
|
private
|
40
52
|
|
41
53
|
def fetch_tables_count
|
@@ -1,21 +1,6 @@
|
|
1
1
|
module Dbviewer
|
2
2
|
class EntityRelationshipDiagramsController < ApplicationController
|
3
3
|
def index
|
4
|
-
# Only show warning if no tables exist, but don't fetch relationships on initial load
|
5
|
-
if @tables.blank?
|
6
|
-
flash.now[:warning] = "No tables found in database to generate ERD."
|
7
|
-
end
|
8
|
-
|
9
|
-
respond_to do |format|
|
10
|
-
format.html # Just render the HTML without relationships
|
11
|
-
format.json do
|
12
|
-
# For JSON requests, return just tables initially
|
13
|
-
render json: {
|
14
|
-
tables: @tables,
|
15
|
-
relationships: []
|
16
|
-
}
|
17
|
-
end
|
18
|
-
end
|
19
4
|
end
|
20
5
|
end
|
21
6
|
end
|
@@ -1,13 +1,11 @@
|
|
1
1
|
module Dbviewer
|
2
2
|
class TablesController < ApplicationController
|
3
|
-
include Dbviewer::PaginationConcern
|
4
|
-
|
5
3
|
before_action :set_table_name, except: [ :index ]
|
6
4
|
before_action :set_query_filters, only: [ :show, :export_csv ]
|
7
5
|
before_action :set_global_filters, only: [ :show, :export_csv ]
|
8
6
|
|
9
7
|
def index
|
10
|
-
@tables = fetch_tables(include_record_counts
|
8
|
+
@tables = fetch_tables(:include_record_counts)
|
11
9
|
end
|
12
10
|
|
13
11
|
def show
|
@@ -18,44 +16,17 @@ module Dbviewer
|
|
18
16
|
direction: @order_direction,
|
19
17
|
column_filters: @column_filters.reject { |_, v| v.blank? }
|
20
18
|
)
|
21
|
-
|
22
|
-
# Get all datatable data in one method call
|
23
19
|
datatable_data = fetch_datatable_data(@table_name, query_params)
|
24
20
|
|
25
|
-
# Assign to instance variables for view access
|
26
21
|
@total_count = datatable_data[:total_count]
|
27
22
|
@records = datatable_data[:records]
|
28
23
|
@total_pages = datatable_data[:total_pages]
|
29
24
|
@columns = datatable_data[:columns]
|
30
25
|
@metadata = datatable_data[:metadata]
|
31
|
-
|
32
|
-
respond_to do |format|
|
33
|
-
format.html # Default HTML response
|
34
|
-
format.json do
|
35
|
-
render json: {
|
36
|
-
table_name: @table_name,
|
37
|
-
columns: @columns,
|
38
|
-
metadata: @metadata,
|
39
|
-
record_count: @total_count
|
40
|
-
}
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def mini_erd
|
46
|
-
@erd_data = fetch_mini_erd_for_table(@table_name)
|
47
|
-
|
48
|
-
respond_to do |format|
|
49
|
-
format.json { render json: @erd_data }
|
50
|
-
format.html { render layout: false }
|
51
|
-
end
|
52
26
|
end
|
53
27
|
|
54
28
|
def query
|
55
|
-
@read_only_mode = true # Flag to indicate we're in read-only mode
|
56
29
|
@columns = fetch_table_columns(@table_name)
|
57
|
-
@tables = fetch_tables # Fetch tables for sidebar
|
58
|
-
|
59
30
|
@query = prepare_query(@table_name, params[:query])
|
60
31
|
@records = execute_query(@query)
|
61
32
|
|
@@ -95,11 +66,11 @@ module Dbviewer
|
|
95
66
|
|
96
67
|
def set_query_filters
|
97
68
|
@current_page = [ 1, params[:page].to_i ].max
|
98
|
-
@per_page = params[:per_page] ? params[:per_page].to_i :
|
99
|
-
@per_page =
|
69
|
+
@per_page = params[:per_page] ? params[:per_page].to_i : Dbviewer.configuration.default_per_page
|
70
|
+
@per_page = Dbviewer.configuration.default_per_page unless Dbviewer.configuration.per_page_options.include?(@per_page)
|
100
71
|
@order_by = params[:order_by].presence || determine_default_order_column
|
101
72
|
@order_direction = params[:order_direction].upcase if params[:order_direction].present?
|
102
|
-
@order_direction = "DESC" unless
|
73
|
+
@order_direction = "DESC" unless %w[ASC DESC].include?(@order_direction)
|
103
74
|
@column_filters = params[:column_filters].presence ? params[:column_filters].to_enum.to_h : {}
|
104
75
|
end
|
105
76
|
|
@@ -40,8 +40,8 @@ module Dbviewer
|
|
40
40
|
end
|
41
41
|
|
42
42
|
# Render a cell that may include a foreign key link
|
43
|
-
def render_table_cell(cell, column_name, metadata)
|
44
|
-
cell_value = format_cell_value(cell)
|
43
|
+
def render_table_cell(cell, column_name, metadata, table_name = nil)
|
44
|
+
cell_value = format_cell_value(cell, table_name, column_name)
|
45
45
|
foreign_key = metadata && metadata[:foreign_keys] ?
|
46
46
|
metadata[:foreign_keys].find { |fk| fk[:column] == column_name } :
|
47
47
|
nil
|
@@ -61,15 +61,15 @@ module Dbviewer
|
|
61
61
|
end
|
62
62
|
|
63
63
|
# Render a table row with cells
|
64
|
-
def render_table_row(row, records, metadata)
|
64
|
+
def render_table_row(row, records, metadata, table_name = nil)
|
65
65
|
content_tag(:tr) do
|
66
66
|
# Start with action column (sticky first column)
|
67
|
-
cells = [ render_action_cell(row, records.columns, metadata) ]
|
67
|
+
cells = [ render_action_cell(row, records.columns, metadata, table_name) ]
|
68
68
|
|
69
69
|
# Add all data cells
|
70
70
|
cells += row.each_with_index.map do |cell, cell_index|
|
71
71
|
column_name = records.columns[cell_index]
|
72
|
-
render_table_cell(cell, column_name, metadata)
|
72
|
+
render_table_cell(cell, column_name, metadata, table_name)
|
73
73
|
end
|
74
74
|
|
75
75
|
cells.join.html_safe
|
@@ -77,7 +77,7 @@ module Dbviewer
|
|
77
77
|
end
|
78
78
|
|
79
79
|
# Render the entire table body with rows
|
80
|
-
def render_table_body(records, metadata)
|
80
|
+
def render_table_body(records, metadata, table_name = nil)
|
81
81
|
if records.nil? || records.rows.nil? || records.empty?
|
82
82
|
content_tag(:tbody) do
|
83
83
|
content_tag(:tr) do
|
@@ -89,19 +89,20 @@ module Dbviewer
|
|
89
89
|
else
|
90
90
|
content_tag(:tbody) do
|
91
91
|
records.rows.map do |row|
|
92
|
-
render_table_row(row, records, metadata)
|
92
|
+
render_table_row(row, records, metadata, table_name)
|
93
93
|
end.join.html_safe
|
94
94
|
end
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
98
98
|
# Render action buttons for a record
|
99
|
-
def render_action_cell(row_data, columns, metadata = nil)
|
99
|
+
def render_action_cell(row_data, columns, metadata = nil, table_name = nil)
|
100
100
|
data_attributes = {}
|
101
101
|
|
102
102
|
# Create a hash of column_name: value pairs for data attributes
|
103
|
+
# Apply the same formatting logic used in table cells
|
103
104
|
columns.each_with_index do |column_name, index|
|
104
|
-
data_attributes[column_name] = row_data[index]
|
105
|
+
data_attributes[column_name] = format_cell_value(row_data[index], table_name, column_name)
|
105
106
|
end
|
106
107
|
|
107
108
|
content_tag(:td, class: "text-center action-column") do
|
@@ -1,6 +1,11 @@
|
|
1
1
|
module Dbviewer
|
2
2
|
module FormattingHelper
|
3
|
-
def format_cell_value(value)
|
3
|
+
def format_cell_value(value, table_name = nil, column_name = nil)
|
4
|
+
# Apply PII masking if configured
|
5
|
+
if table_name && column_name
|
6
|
+
value = Dbviewer::DataPrivacy::PiiMasker.mask_value(value, table_name, column_name)
|
7
|
+
end
|
8
|
+
|
4
9
|
return "NULL" if value.nil?
|
5
10
|
return format_default_value(value) unless value.is_a?(String)
|
6
11
|
|
@@ -89,5 +89,5 @@
|
|
89
89
|
</div>
|
90
90
|
|
91
91
|
<input type="text" id="tables" class="d-none" value='<%= raw @tables.to_json %>'>
|
92
|
-
<input type="text" id="tables_path" class="d-none" value='<%= dbviewer.
|
92
|
+
<input type="text" id="tables_path" class="d-none" value='<%= dbviewer.api_tables_path %>'>
|
93
93
|
<input type="text" id="relationships_api_path" class="d-none" value='<%= dbviewer.relationships_api_entity_relationship_diagrams_path %>'>
|
@@ -5,7 +5,18 @@
|
|
5
5
|
<% content_for :head do %>
|
6
6
|
<link href="https://cdn.jsdelivr.net/npm/vscode-codicons@0.0.17/dist/codicon.min.css" rel="stylesheet">
|
7
7
|
<%= stylesheet_link_tag "dbviewer/query", "data-turbo-track": "reload" %>
|
8
|
-
|
8
|
+
<!-- Load Monaco editor first as a regular script -->
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/min/vs/loader.js"></script>
|
10
|
+
<script>
|
11
|
+
// Set up Monaco loader
|
12
|
+
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/min/vs' } });
|
13
|
+
window.MonacoEnvironment = { getWorkerUrl: () => proxy };
|
14
|
+
let proxy = URL.createObjectURL(new Blob([`
|
15
|
+
self.MonacoEnvironment = { baseUrl: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/min/' };
|
16
|
+
importScripts('https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/min/vs/base/worker/workerMain.js');
|
17
|
+
`], { type: 'text/javascript' }));
|
18
|
+
</script>
|
19
|
+
<%= javascript_include_tag "dbviewer/query", "data-turbo-track": "reload" %>
|
9
20
|
<% end %>
|
10
21
|
|
11
22
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
@@ -26,6 +37,7 @@
|
|
26
37
|
<div class="mb-3">
|
27
38
|
<div id="monaco-editor" class="monaco-editor-container" style="min-height: 200px; border-radius: 4px; margin-bottom: 0rem;"
|
28
39
|
data-initial-query="<%= CGI.escapeHTML(@query.to_s) %>"></div>
|
40
|
+
<div id="query-container" class="editor-error-container"></div>
|
29
41
|
<%= form.hidden_field :query, id: "query-input", value: @query.to_s %>
|
30
42
|
</div>
|
31
43
|
|
@@ -100,11 +112,14 @@
|
|
100
112
|
</div>
|
101
113
|
</div>
|
102
114
|
|
103
|
-
|
104
|
-
|
105
|
-
<
|
106
|
-
|
107
|
-
|
115
|
+
<div id="query-results">
|
116
|
+
<% if @error.present? %>
|
117
|
+
<div class="alert alert-danger" role="alert">
|
118
|
+
<i class="bi bi-exclamation-triangle me-2"></i>
|
119
|
+
<strong>Error:</strong> <%= @error %>
|
120
|
+
</div>
|
121
|
+
<% end %>
|
122
|
+
</div>
|
108
123
|
|
109
124
|
<% if @records.present? %>
|
110
125
|
<div class="card">
|
@@ -127,8 +142,9 @@
|
|
127
142
|
<% if @records.rows.any? %>
|
128
143
|
<% @records.rows.each do |row| %>
|
129
144
|
<tr>
|
130
|
-
<% row.
|
131
|
-
|
145
|
+
<% row.each_with_index do |cell, index| %>
|
146
|
+
<% column_name = @records.columns[index] %>
|
147
|
+
<td><%= format_cell_value(cell, @table_name, column_name) %></td>
|
132
148
|
<% end %>
|
133
149
|
</tr>
|
134
150
|
<% end %>
|