dbviewer 0.7.10 → 0.7.11
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 +1 -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/views/dbviewer/entity_relationship_diagrams/index.html.erb +1 -1
- data/app/views/dbviewer/tables/query.html.erb +21 -6
- data/app/views/dbviewer/tables/show.html.erb +2 -2
- data/app/views/layouts/dbviewer/application.html.erb +12 -3
- data/config/routes.rb +2 -2
- 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
- metadata +15 -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
|
|
@@ -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">
|
@@ -100,7 +100,7 @@
|
|
100
100
|
<div class="card-header d-flex justify-content-between align-items-center">
|
101
101
|
<h5 class="mb-0">
|
102
102
|
<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) %>'">
|
103
|
-
<% Dbviewer
|
103
|
+
<% Dbviewer.configuration.per_page_options.each do |option| %>
|
104
104
|
<option value="<%= option %>" <%= 'selected' if @per_page == option %>><%= option %></option>
|
105
105
|
<% end %>
|
106
106
|
</select>
|
@@ -348,5 +348,5 @@
|
|
348
348
|
</div>
|
349
349
|
</div>
|
350
350
|
<% end %>
|
351
|
-
<input type="hidden" id="mini_erd_table_path" name="mini_erd_table_path" value="<%= dbviewer.
|
351
|
+
<input type="hidden" id="mini_erd_table_path" name="mini_erd_table_path" value="<%= dbviewer.mini_erd_api_table_path(@table_name, format: :json) %>">
|
352
352
|
<input type="hidden" id="table_name" name="table_name" value="<%= @table_name %>">
|
@@ -5,6 +5,13 @@
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
6
6
|
<%= csrf_meta_tags %>
|
7
7
|
<%= csp_meta_tag %>
|
8
|
+
|
9
|
+
<!-- Favicon -->
|
10
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>👁️</text></svg>">
|
11
|
+
<link rel="icon" href="<%= asset_path('dbviewer/favicon.ico') %>" type="image/x-icon">
|
12
|
+
<link rel="shortcut icon" href="<%= asset_path('dbviewer/favicon.ico') %>" type="image/x-icon">
|
13
|
+
<link rel="icon" type="image/svg+xml" href="<%= asset_path('dbviewer/favicon.svg') %>">
|
14
|
+
<link rel="icon" type="image/png" href="<%= asset_path('dbviewer/favicon.png') %>">
|
8
15
|
|
9
16
|
<!-- Prevent theme flash during page load -->
|
10
17
|
<script>
|
@@ -37,6 +44,8 @@
|
|
37
44
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
38
45
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
39
46
|
|
47
|
+
<%= javascript_include_tag "dbviewer/utility", "data-turbo-track": "reload" %>
|
48
|
+
<%= javascript_include_tag "dbviewer/error_handler", "data-turbo-track": "reload" %>
|
40
49
|
<%= javascript_include_tag "dbviewer/layout", "data-turbo-track": "reload" %>
|
41
50
|
<%= javascript_include_tag "dbviewer/sidebar", "data-turbo-track": "reload" %>
|
42
51
|
<%= stylesheet_link_tag "dbviewer/application", "data-turbo-track": "reload" %>
|
@@ -47,8 +56,8 @@
|
|
47
56
|
<div class="dbviewer-wrapper">
|
48
57
|
<!-- Top Navigation Bar (Fixed) -->
|
49
58
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary dbviewer-navbar fixed-top">
|
50
|
-
<div class="container-fluid">
|
51
|
-
<a class="navbar-brand" href="<%= dbviewer.root_path %>"><
|
59
|
+
<div class="container-fluid px-0">
|
60
|
+
<a class="navbar-brand d-flex" href="<%= dbviewer.root_path %>"><span class="me-2">👁️</span> DB Viewer</a>
|
52
61
|
<div class="d-flex align-items-center">
|
53
62
|
<button class="navbar-toggler border-0 px-2 d-lg-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#navbarOffcanvas"
|
54
63
|
aria-controls="navbarOffcanvas" aria-expanded="false" aria-label="Toggle navigation">
|
@@ -110,7 +119,7 @@
|
|
110
119
|
<!-- Offcanvas sidebar for mobile/tablet view that slides from right -->
|
111
120
|
<div class="offcanvas offcanvas-end d-lg-none" tabindex="-1" id="navbarOffcanvas" aria-labelledby="offcanvasNavbarLabel">
|
112
121
|
<div class="offcanvas-header bg-light-subtle">
|
113
|
-
<h5 class="offcanvas-title" id="offcanvasNavbarLabel"
|
122
|
+
<h5 class="offcanvas-title" id="offcanvasNavbarLabel">👁️ DB Viewer</h5>
|
114
123
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
115
124
|
</div>
|
116
125
|
<div class="offcanvas-body bg-body-tertiary">
|