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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -0
  3. data/app/assets/images/dbviewer/emoji-favicon.txt +1 -0
  4. data/app/assets/images/dbviewer/favicon.ico +4 -0
  5. data/app/assets/images/dbviewer/favicon.png +4 -0
  6. data/app/assets/images/dbviewer/favicon.svg +10 -0
  7. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +38 -42
  8. data/app/assets/javascripts/dbviewer/error_handler.js +58 -0
  9. data/app/assets/javascripts/dbviewer/home.js +25 -34
  10. data/app/assets/javascripts/dbviewer/layout.js +100 -129
  11. data/app/assets/javascripts/dbviewer/query.js +309 -246
  12. data/app/assets/javascripts/dbviewer/sidebar.js +170 -183
  13. data/app/assets/javascripts/dbviewer/utility.js +124 -0
  14. data/app/assets/stylesheets/dbviewer/application.css +8 -146
  15. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +0 -34
  16. data/app/assets/stylesheets/dbviewer/logs.css +0 -11
  17. data/app/assets/stylesheets/dbviewer/query.css +21 -9
  18. data/app/assets/stylesheets/dbviewer/table.css +49 -131
  19. data/app/controllers/concerns/dbviewer/database_operations/connection_management.rb +90 -0
  20. data/app/controllers/concerns/dbviewer/database_operations/data_export.rb +31 -0
  21. data/app/controllers/concerns/dbviewer/database_operations/database_information.rb +54 -0
  22. data/app/controllers/concerns/dbviewer/database_operations/datatable_operations.rb +37 -0
  23. data/app/controllers/concerns/dbviewer/database_operations/query_operations.rb +37 -0
  24. data/app/controllers/concerns/dbviewer/database_operations/relationship_management.rb +175 -0
  25. data/app/controllers/concerns/dbviewer/database_operations/table_operations.rb +46 -0
  26. data/app/controllers/concerns/dbviewer/database_operations.rb +4 -9
  27. data/app/controllers/dbviewer/api/tables_controller.rb +12 -0
  28. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +0 -15
  29. data/app/controllers/dbviewer/tables_controller.rb +4 -33
  30. data/app/helpers/dbviewer/datatable_ui_table_helper.rb +10 -9
  31. data/app/helpers/dbviewer/formatting_helper.rb +6 -1
  32. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +1 -1
  33. data/app/views/dbviewer/tables/query.html.erb +24 -8
  34. data/app/views/dbviewer/tables/show.html.erb +3 -3
  35. data/app/views/layouts/dbviewer/application.html.erb +12 -3
  36. data/config/routes.rb +2 -2
  37. data/lib/dbviewer/configuration.rb +21 -0
  38. data/lib/dbviewer/data_privacy/pii_masker.rb +125 -0
  39. data/lib/dbviewer/database/manager.rb +2 -2
  40. data/lib/dbviewer/datatable/query_operations.rb +1 -17
  41. data/lib/dbviewer/engine.rb +29 -0
  42. data/lib/dbviewer/version.rb +1 -1
  43. data/lib/dbviewer.rb +45 -0
  44. data/lib/generators/dbviewer/install_generator.rb +6 -0
  45. data/lib/generators/dbviewer/templates/pii_configuration_example.rb +99 -0
  46. metadata +17 -10
  47. data/app/controllers/concerns/dbviewer/connection_management.rb +0 -88
  48. data/app/controllers/concerns/dbviewer/data_export.rb +0 -32
  49. data/app/controllers/concerns/dbviewer/database_information.rb +0 -62
  50. data/app/controllers/concerns/dbviewer/datatable_support.rb +0 -47
  51. data/app/controllers/concerns/dbviewer/pagination_concern.rb +0 -34
  52. data/app/controllers/concerns/dbviewer/query_operations.rb +0 -28
  53. data/app/controllers/concerns/dbviewer/relationship_management.rb +0 -173
  54. 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 TableOperations
8
- include RelationshipManagement
8
+ include DatatableOperations
9
9
  include QueryOperations
10
- include DataExport
11
- include DatatableSupport
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: true)
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 : self.class.default_per_page
99
- @per_page = self.class.default_per_page unless self.class.per_page_options.include?(@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 self.class::VALID_SORT_DIRECTIONS.include?(@order_direction)
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].to_s
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.tables_path %>'>
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
- <%= javascript_include_tag "dbviewer/query", "data-turbo-track": "reload", type: :module %>
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
- <% if @error.present? %>
104
- <div class="alert alert-danger" role="alert">
105
- <strong>Error:</strong> <%= @error %>
106
- </div>
107
- <% end %>
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.each do |cell| %>
131
- <td><%= format_cell_value(cell) %></td>
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 %>