pg_reports 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0a634540a400676a49a8db9574fe8abbccd9faeae920e5d22323b1d54c791bd
4
- data.tar.gz: ec2665323a7adbc2eae7dbcdd2a0b49ca5d7e4a96ca3f9603b1fd4304c23c98b
3
+ metadata.gz: 76f8945d09a2245c88f64ac0a07634055f7fb3133b87f15d7c7ca89915ace112
4
+ data.tar.gz: 28d4080e6ed159f24b3df313237b312218acdfc0c1681157db81af1aa4f09aab
5
5
  SHA512:
6
- metadata.gz: ff18cbb7ffe10932d42b8a277fb33ef812a7d8d4211874575ea51869d9478c868143b7e912d639c493eb4ee81252b6a0c296aaaeefd132aa381bb6e5b99aef99
7
- data.tar.gz: ef94bcbc4e686d1c82f6e6664735dbdbdbdbd441285be485c43fa593c69ba47886d16dd25998776ab7ef772382ec7e97d472fa34011b8683ee0c7d88881ec69b
6
+ metadata.gz: 3591a546c84af405ffa25664aa3016e85297608ba12130d6bb0aa8f0fc2f39b5c91c8470b672b1d67a24c834638384adf658062b08a582814d1271b8c1a5d9d1
7
+ data.tar.gz: 3e90622e7676573d94b2ef2c7d5e2b7070bb9a9d5fef021440aab741bfc5faa15d9b3b6a5fbcba8883c98484f1096f0d93ec048a3db3d0b728deed4d1880cb72
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2026-01-28
9
+
10
+ ### Added
11
+
12
+ - Sortable table columns - click on column header to sort ascending/descending
13
+ - Top scrollbar for wide tables (synchronized with bottom scroll)
14
+ - Report descriptions in dashboard cards on the main page
15
+ - IDE integration for source code links:
16
+ - VS Code (WSL) - for Windows Subsystem for Linux
17
+ - VS Code - direct path for native Linux
18
+ - RubyMine
19
+ - IntelliJ IDEA
20
+ - Cursor
21
+ - IDE settings modal to choose default IDE (skip menu and open directly)
22
+ - Save records for comparison - save query results to compare before/after optimizations
23
+ - EXPLAIN ANALYZE for queries - run EXPLAIN ANALYZE directly from the dashboard
24
+ - Migration generator for unused/broken indexes - generate and create migration files
25
+
26
+ ### Changed
27
+
28
+ - Reduced spacing between report description and results table
29
+ - Dropdown menus now use fixed positioning to prevent clipping by table rows
30
+
8
31
  ## [0.1.0] - 2026-01-17
9
32
 
10
33
  ### Added
data/README.md CHANGED
@@ -15,9 +15,13 @@ A comprehensive PostgreSQL monitoring and analysis library for Rails application
15
15
  - 📋 **Table Statistics** - Monitor table sizes, bloat, vacuum needs, and cache hit ratios
16
16
  - 🔌 **Connection Monitoring** - Track active connections, locks, and blocking queries
17
17
  - 🖥️ **System Overview** - Database sizes, PostgreSQL settings, installed extensions
18
- - 🌐 **Web Dashboard** - Beautiful dark-themed UI with expandable rows
18
+ - 🌐 **Web Dashboard** - Beautiful dark-themed UI with sortable tables and expandable rows
19
19
  - 📨 **Telegram Integration** - Send reports directly to Telegram
20
20
  - 📥 **Export** - Download reports in TXT, CSV, or JSON format
21
+ - 🔗 **IDE Integration** - Open source locations in VS Code, RubyMine, IntelliJ, or Cursor
22
+ - 📌 **Comparison Mode** - Save records to compare before/after optimization
23
+ - 📊 **EXPLAIN ANALYZE** - Run query plans directly from the dashboard
24
+ - 🗑️ **Migration Generator** - Generate Rails migrations to drop unused indexes
21
25
 
22
26
  ## Installation
23
27
 
@@ -255,13 +259,60 @@ report.select { |row| row["calls"] > 100 }
255
259
 
256
260
  The dashboard provides:
257
261
 
258
- - 📊 Overview of all report categories
262
+ - 📊 Overview of all report categories with descriptions
259
263
  - ⚡ One-click report execution
260
264
  - 🔍 Expandable rows for full query text
261
265
  - 📋 Copy query to clipboard
262
- - 📥 Download in multiple formats
266
+ - 📥 Download in multiple formats (TXT, CSV, JSON)
263
267
  - 📨 Send to Telegram
264
268
  - 🔧 pg_stat_statements management
269
+ - 🔄 Sortable columns - click headers to sort ascending/descending
270
+ - 📌 Save records for comparison - track before/after optimization results
271
+ - 📊 EXPLAIN ANALYZE - run query plans directly from the dashboard
272
+ - 🗑️ Migration generator - create Rails migrations to drop unused indexes
273
+ - 🔗 IDE integration - click source locations to open in your IDE
274
+
275
+ ### IDE Integration
276
+
277
+ Click on source locations in reports to open the file directly in your IDE. Supported IDEs:
278
+
279
+ - **VS Code (WSL)** - for Windows Subsystem for Linux
280
+ - **VS Code** - direct path for native Linux/macOS
281
+ - **RubyMine**
282
+ - **IntelliJ IDEA**
283
+ - **Cursor**
284
+
285
+ Use the ⚙️ button to set your default IDE and skip the selection menu.
286
+
287
+ ### Save Records for Comparison
288
+
289
+ When optimizing queries, you can save records to compare before/after results:
290
+
291
+ 1. Expand a row and click "📌 Save for Comparison"
292
+ 2. Saved records appear above the results table
293
+ 3. Click saved records to expand and see all details
294
+ 4. Clear all or remove individual saved records
295
+
296
+ Records are stored in browser localStorage per report type.
297
+
298
+ ### EXPLAIN ANALYZE
299
+
300
+ For query reports, you can run EXPLAIN ANALYZE directly:
301
+
302
+ 1. Expand a row with a query
303
+ 2. Click "📊 EXPLAIN ANALYZE"
304
+ 3. View the execution plan with timing statistics
305
+
306
+ > Note: Queries with parameter placeholders ($1, $2) from pg_stat_statements cannot be analyzed directly. Copy the query and replace parameters with actual values.
307
+
308
+ ### Migration Generator
309
+
310
+ For unused or invalid indexes, generate Rails migrations:
311
+
312
+ 1. Go to Indexes → Unused Indexes
313
+ 2. Expand a row and click "🗑️ Generate Migration"
314
+ 3. Copy the code or create the file directly
315
+ 4. The file opens automatically in your configured IDE
265
316
 
266
317
  ### Authentication
267
318
 
@@ -33,6 +33,11 @@ module PgReports
33
33
  return
34
34
  end
35
35
 
36
+ # Get documentation for the report
37
+ @documentation = Dashboard::ReportsRegistry.documentation(@report_key)
38
+ @thresholds = Dashboard::ReportsRegistry.thresholds(@report_key)
39
+ @problem_fields = Dashboard::ReportsRegistry.problem_fields(@report_key)
40
+
36
41
  @report = execute_report(@category, @report_key)
37
42
  rescue => e
38
43
  @error = e.message
@@ -44,6 +49,8 @@ module PgReports
44
49
  report_key = params[:report].to_sym
45
50
 
46
51
  report = execute_report(category, report_key)
52
+ thresholds = Dashboard::ReportsRegistry.thresholds(report_key)
53
+ problem_fields = Dashboard::ReportsRegistry.problem_fields(report_key)
47
54
 
48
55
  render json: {
49
56
  success: true,
@@ -51,7 +58,9 @@ module PgReports
51
58
  columns: report.columns,
52
59
  data: report.data.first(100),
53
60
  total: report.size,
54
- generated_at: report.generated_at.strftime("%Y-%m-%d %H:%M:%S")
61
+ generated_at: report.generated_at.strftime("%Y-%m-%d %H:%M:%S"),
62
+ thresholds: thresholds,
63
+ problem_fields: problem_fields
55
64
  }
56
65
  rescue => e
57
66
  render json: {success: false, error: e.message}, status: :unprocessable_entity
@@ -103,6 +112,86 @@ module PgReports
103
112
  render json: {success: false, error: e.message}, status: :unprocessable_entity
104
113
  end
105
114
 
115
+ def explain_analyze
116
+ query = params[:query]
117
+
118
+ if query.blank?
119
+ render json: {success: false, error: "Query is required"}, status: :unprocessable_entity
120
+ return
121
+ end
122
+
123
+ # Security: Only allow SELECT queries for EXPLAIN ANALYZE
124
+ normalized = query.strip.gsub(/\s+/, " ").downcase
125
+ unless normalized.start_with?("select")
126
+ render json: {success: false, error: "Only SELECT queries are allowed for EXPLAIN ANALYZE"}, status: :unprocessable_entity
127
+ return
128
+ end
129
+
130
+ # Check for parameterized queries (from pg_stat_statements normalization)
131
+ if query.match?(/\$\d+/)
132
+ render json: {
133
+ success: false,
134
+ error: "This query contains parameter placeholders ($1, $2, etc.) from pg_stat_statements normalization. " \
135
+ "EXPLAIN ANALYZE cannot be run on parameterized queries without actual values. " \
136
+ "Copy the query and replace parameters with real values to analyze it manually."
137
+ }, status: :unprocessable_entity
138
+ return
139
+ end
140
+
141
+ result = ActiveRecord::Base.connection.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{query}")
142
+ explain_output = result.map { |r| r["QUERY PLAN"] }.join("\n")
143
+
144
+ # Extract stats from the output
145
+ stats = {}
146
+ if (match = explain_output.match(/Planning Time: ([\d.]+) ms/))
147
+ stats[:planning_time] = match[1].to_f
148
+ end
149
+ if (match = explain_output.match(/Execution Time: ([\d.]+) ms/))
150
+ stats[:execution_time] = match[1].to_f
151
+ end
152
+ if (match = explain_output.match(/cost=[\d.]+\.\.([\d.]+)/))
153
+ stats[:total_cost] = match[1].to_f
154
+ end
155
+ if (match = explain_output.match(/rows=(\d+)/))
156
+ stats[:rows] = match[1].to_i
157
+ end
158
+
159
+ render json: {success: true, explain: explain_output, stats: stats}
160
+ rescue => e
161
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
162
+ end
163
+
164
+ def create_migration
165
+ file_name = params[:file_name]
166
+ code = params[:code]
167
+
168
+ if file_name.blank? || code.blank?
169
+ render json: {success: false, error: "File name and code are required"}, status: :unprocessable_entity
170
+ return
171
+ end
172
+
173
+ # Sanitize file name
174
+ safe_file_name = file_name.gsub(/[^a-z0-9_.]/, "")
175
+ unless safe_file_name.match?(/\A\d{14}_\w+\.rb\z/)
176
+ render json: {success: false, error: "Invalid migration file name format"}, status: :unprocessable_entity
177
+ return
178
+ end
179
+
180
+ # Find migrations directory
181
+ migrations_path = Rails.root.join("db", "migrate")
182
+ unless migrations_path.exist?
183
+ render json: {success: false, error: "Migrations directory not found"}, status: :unprocessable_entity
184
+ return
185
+ end
186
+
187
+ file_path = migrations_path.join(safe_file_name)
188
+ File.write(file_path, code)
189
+
190
+ render json: {success: true, file_path: file_path.to_s, message: "Migration created successfully"}
191
+ rescue => e
192
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
193
+ end
194
+
106
195
  private
107
196
 
108
197
  def authenticate_dashboard!
@@ -3,8 +3,9 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="pg-reports-root" content="<%= pg_reports.root_path.chomp('/') %>">
6
+ <meta name="pg-reports-root" content="<%= request.script_name.presence || PgReports::Engine.routes.url_helpers.root_path %>">
7
7
  <title>PgReports Dashboard</title>
8
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%234f46e5'/%3E%3Crect x='5' y='18' width='5' height='9' rx='1' fill='%23fff'/%3E%3Crect x='13.5' y='12' width='5' height='15' rx='1' fill='%23fff'/%3E%3Crect x='22' y='6' width='5' height='21' rx='1' fill='%23fff'/%3E%3C/svg%3E">
8
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
11
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -105,12 +105,18 @@ pg_stat_statements.track = all</pre>
105
105
  <% category[:reports].each do |report_key, report| %>
106
106
  <% if category_key == :queries && !@pg_stat_status[:ready] %>
107
107
  <div class="report-link disabled">
108
- <span><%= report[:name] %></span>
108
+ <div class="report-link-info">
109
+ <span class="report-link-name"><%= report[:name] %></span>
110
+ <span class="report-link-desc"><%= report[:description] %></span>
111
+ </div>
109
112
  <span class="lock">🔒</span>
110
113
  </div>
111
114
  <% else %>
112
115
  <%= link_to report_path(category: category_key, report: report_key), class: "report-link" do %>
113
- <span><%= report[:name] %></span>
116
+ <div class="report-link-info">
117
+ <span class="report-link-name"><%= report[:name] %></span>
118
+ <span class="report-link-desc"><%= report[:description] %></span>
119
+ </div>
114
120
  <span class="arrow">→</span>
115
121
  <% end %>
116
122
  <% end %>
@@ -216,6 +222,31 @@ pg_stat_statements.track = all</pre>
216
222
  opacity: 0.5;
217
223
  }
218
224
 
225
+ .report-link-info {
226
+ display: flex;
227
+ flex-direction: column;
228
+ gap: 0.125rem;
229
+ flex: 1;
230
+ min-width: 0;
231
+ }
232
+
233
+ .report-link-name {
234
+ font-weight: 500;
235
+ color: inherit;
236
+ }
237
+
238
+ .report-link-desc {
239
+ font-size: 0.75rem;
240
+ color: var(--text-muted);
241
+ white-space: nowrap;
242
+ overflow: hidden;
243
+ text-overflow: ellipsis;
244
+ }
245
+
246
+ .report-link:hover .report-link-desc {
247
+ color: var(--text-secondary);
248
+ }
249
+
219
250
  /* Modal */
220
251
  .modal {
221
252
  position: fixed;