pg_insights 0.3.1 → 0.4.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/pg_insights/application.js +91 -24
  3. data/app/assets/javascripts/pg_insights/plan_performance.js +53 -0
  4. data/app/assets/javascripts/pg_insights/query_comparison.js +1129 -0
  5. data/app/assets/javascripts/pg_insights/results/view_toggles.js +26 -5
  6. data/app/assets/javascripts/pg_insights/results.js +231 -2
  7. data/app/assets/stylesheets/pg_insights/analysis.css +2628 -0
  8. data/app/assets/stylesheets/pg_insights/application.css +51 -1
  9. data/app/assets/stylesheets/pg_insights/results.css +12 -1
  10. data/app/controllers/pg_insights/insights_controller.rb +486 -9
  11. data/app/helpers/pg_insights/application_helper.rb +339 -0
  12. data/app/helpers/pg_insights/insights_helper.rb +567 -0
  13. data/app/jobs/pg_insights/query_analysis_job.rb +142 -0
  14. data/app/models/pg_insights/query_execution.rb +198 -0
  15. data/app/services/pg_insights/query_analysis_service.rb +269 -0
  16. data/app/views/layouts/pg_insights/application.html.erb +8 -1
  17. data/app/views/pg_insights/insights/_compare_view.html.erb +264 -0
  18. data/app/views/pg_insights/insights/_empty_state.html.erb +9 -0
  19. data/app/views/pg_insights/insights/_execution_table_view.html.erb +86 -0
  20. data/app/views/pg_insights/insights/_history_bar.html.erb +33 -0
  21. data/app/views/pg_insights/insights/_perf_view.html.erb +244 -0
  22. data/app/views/pg_insights/insights/_plan_nodes.html.erb +12 -0
  23. data/app/views/pg_insights/insights/_plan_tree.html.erb +30 -0
  24. data/app/views/pg_insights/insights/_plan_tree_modern.html.erb +12 -0
  25. data/app/views/pg_insights/insights/_plan_view.html.erb +159 -0
  26. data/app/views/pg_insights/insights/_query_panel.html.erb +3 -2
  27. data/app/views/pg_insights/insights/_result.html.erb +19 -4
  28. data/app/views/pg_insights/insights/_results_info.html.erb +33 -9
  29. data/app/views/pg_insights/insights/_results_info_empty.html.erb +10 -0
  30. data/app/views/pg_insights/insights/_results_panel.html.erb +7 -9
  31. data/app/views/pg_insights/insights/_results_table.html.erb +0 -5
  32. data/app/views/pg_insights/insights/_visual_view.html.erb +212 -0
  33. data/app/views/pg_insights/insights/index.html.erb +4 -1
  34. data/app/views/pg_insights/timeline/compare.html.erb +3 -3
  35. data/config/routes.rb +6 -0
  36. data/lib/generators/pg_insights/install_generator.rb +20 -14
  37. data/lib/generators/pg_insights/templates/db/migrate/create_pg_insights_query_executions.rb +45 -0
  38. data/lib/pg_insights/version.rb +1 -1
  39. data/lib/pg_insights.rb +30 -2
  40. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 202029a129c23bfda08834a7867975d0e985789e516dcccd24391e9798ef1f63
4
- data.tar.gz: 6f72d9f5affa0bd332a2e14d1c609e46741ae87cbabcc70086ce6263d4c3954e
3
+ metadata.gz: 92152260b0987ce757b707833297038fee48d0dbebd8ea727012052eaa3ee6f2
4
+ data.tar.gz: 971daab4babe4ef099c544a3cf2bd0a7c64b332c6785a7b33dc9e90463f31b25
5
5
  SHA512:
6
- metadata.gz: 2a934c35cbd99830313711b601a0b50de06da05aa09d1cd7c49de42bf37d816e224333c13b27d92da623070caadb433ed844789f00d8080804d16a9d36c2ce15
7
- data.tar.gz: 892adeea3391c59f4932bfd1aa789523cfe7390cd8b91401e356bc2ac3a0a15ac54084c4eb9fb2fa662c5939c73c73fcb904881addbb9473d41d8e946d8157c1
6
+ metadata.gz: d562ca875f72512bd5f422a3eb87b835496c808b09282f7f6014ed39deaeeba5afc116f179a08e75a402e49014fff4c1872aabf52a2a693b3b7175eff09d86c7
7
+ data.tar.gz: d404b95da5502eb7b9a966f0b6eb43e59fb35c9e637f4b77d71d6a6cc06149430a7ce3d73cadc9c9375902bdec785191007cb5e95a9dd41641901d7dcc49484e
@@ -1,7 +1,3 @@
1
- //= require chartkick
2
- //= require Chart.bundle
3
- //= require_tree .
4
-
5
1
 
6
2
  // PG Insights JavaScript
7
3
  document.addEventListener('DOMContentLoaded', function() {
@@ -26,6 +22,45 @@ document.addEventListener('DOMContentLoaded', function() {
26
22
  this.validateInitialQuery();
27
23
  this.setupQueryExamples();
28
24
  this.loadTableNames();
25
+ this.initializeAnalysisViews();
26
+ },
27
+
28
+ // Initialize analysis views if present
29
+ initializeAnalysisViews() {
30
+ const planView = document.getElementById('plan-view');
31
+ const perfView = document.getElementById('perf-view');
32
+ const planTab = document.querySelector('[data-view="plan"]');
33
+
34
+ if (planView && planTab && planView.style.display !== 'none') {
35
+ // Plan view is visible, make sure the tab is properly activated
36
+ this.activateAnalysisTab('plan');
37
+ }
38
+ },
39
+
40
+ // Activate analysis tab
41
+ activateAnalysisTab(viewType) {
42
+ const allTabs = document.querySelectorAll('.toggle-btn');
43
+ const targetTab = document.querySelector(`[data-view="${viewType}"]`);
44
+
45
+ if (targetTab) {
46
+ allTabs.forEach(tab => tab.classList.remove('active'));
47
+ targetTab.classList.add('active');
48
+
49
+ const allViews = document.querySelectorAll('.view-content');
50
+ allViews.forEach(view => view.style.display = 'none');
51
+
52
+ const targetView = document.getElementById(`${viewType}-view`);
53
+ if (targetView) {
54
+ targetView.style.display = 'block';
55
+
56
+ // Initialize PEV2 if switching to visual view
57
+ if (viewType === 'visual' && typeof window.initPEV2 !== 'undefined') {
58
+ setTimeout(() => {
59
+ window.initPEV2();
60
+ }, 100);
61
+ }
62
+ }
63
+ }
29
64
  },
30
65
 
31
66
  // Load queries from data attribute
@@ -46,7 +81,7 @@ document.addEventListener('DOMContentLoaded', function() {
46
81
  // Keep this for backward compatibility if needed elsewhere
47
82
  },
48
83
 
49
- // Copy current query functionality
84
+
50
85
  copyCurrentQuery() {
51
86
  const textarea = document.querySelector('.sql-editor');
52
87
  const btn = document.querySelector('.btn-icon.btn-copy');
@@ -64,7 +99,7 @@ document.addEventListener('DOMContentLoaded', function() {
64
99
  });
65
100
  },
66
101
 
67
- // Save or Update the current query
102
+
68
103
  saveCurrentQuery() {
69
104
  const textarea = document.querySelector('.sql-editor');
70
105
  const sql = textarea?.value.trim();
@@ -79,7 +114,7 @@ document.addEventListener('DOMContentLoaded', function() {
79
114
  name = prompt("Enter a name for this new saved query:");
80
115
  }
81
116
 
82
- if (!name) return; // User cancelled prompt
117
+ if (!name) return;
83
118
 
84
119
  const method = isUpdate ? 'PATCH' : 'POST';
85
120
  const url = isUpdate ? `/pg_insights/queries/${this.currentQueryState.id}` : '/pg_insights/queries';
@@ -133,7 +168,7 @@ document.addEventListener('DOMContentLoaded', function() {
133
168
  });
134
169
  },
135
170
 
136
- // Query validation
171
+
137
172
  validateQuery(sql) {
138
173
  if (!sql || !sql.trim()) {
139
174
  return { valid: false, message: "Please enter a SQL query" };
@@ -209,7 +244,7 @@ document.addEventListener('DOMContentLoaded', function() {
209
244
  }
210
245
  },
211
246
 
212
- // Query management
247
+
213
248
  clearQuery() {
214
249
  const textarea = document.querySelector('.sql-editor');
215
250
  if (textarea) {
@@ -255,7 +290,7 @@ document.addEventListener('DOMContentLoaded', function() {
255
290
  });
256
291
  },
257
292
 
258
- // Preview selected table
293
+
259
294
  previewTable(tableName) {
260
295
  if (!tableName) return;
261
296
 
@@ -265,12 +300,8 @@ document.addEventListener('DOMContentLoaded', function() {
265
300
  if (textarea) {
266
301
  textarea.value = sql;
267
302
 
268
- // Validate the query
269
303
  this.validateAndUpdateUI(sql);
270
-
271
- // Auto-resize textarea
272
- textarea.style.height = 'auto';
273
- textarea.style.height = Math.max(160, textarea.scrollHeight) + 'px';
304
+ this.resizeTextarea(textarea);
274
305
 
275
306
  // Auto-execute the query
276
307
  const executeBtn = document.getElementById('execute-btn');
@@ -286,6 +317,35 @@ document.addEventListener('DOMContentLoaded', function() {
286
317
  }
287
318
  },
288
319
 
320
+ resizeTextarea(textarea) {
321
+ if (!textarea) return;
322
+
323
+ const formContent = textarea.closest('.form-content');
324
+ if (!formContent) {
325
+ textarea.style.height = 'auto';
326
+ textarea.style.height = Math.max(160, Math.min(400, textarea.scrollHeight)) + 'px';
327
+ return;
328
+ }
329
+
330
+ const formContentStyle = window.getComputedStyle(formContent);
331
+ const formContentHeight = parseInt(formContentStyle.height);
332
+ const formContentPadding = parseInt(formContentStyle.paddingTop) + parseInt(formContentStyle.paddingBottom);
333
+ const availableHeight = formContentHeight - formContentPadding;
334
+
335
+ const minHeight = 160;
336
+ const maxHeight = Math.min(400, Math.max(minHeight, availableHeight - 20));
337
+
338
+ textarea.style.height = 'auto';
339
+ const idealHeight = Math.max(minHeight, Math.min(maxHeight, textarea.scrollHeight));
340
+ textarea.style.height = idealHeight + 'px';
341
+
342
+ if (textarea.scrollHeight > maxHeight) {
343
+ textarea.style.overflowY = 'auto';
344
+ } else {
345
+ textarea.style.overflowY = 'hidden';
346
+ }
347
+ },
348
+
289
349
  loadQueryById(queryId) {
290
350
  const query = this.config.queries.find(q => q.id.toString() === queryId.toString());
291
351
 
@@ -361,7 +421,7 @@ document.addEventListener('DOMContentLoaded', function() {
361
421
  });
362
422
  },
363
423
 
364
- // Event binding
424
+
365
425
  bindEvents() {
366
426
  const textarea = document.querySelector('.sql-editor');
367
427
  const executeBtn = document.getElementById('execute-btn');
@@ -369,24 +429,20 @@ document.addEventListener('DOMContentLoaded', function() {
369
429
  // Real-time validation on input
370
430
  if (textarea) {
371
431
  textarea.addEventListener('input', () => {
372
- // Auto-resize
373
- textarea.style.height = 'auto';
374
- textarea.style.height = Math.max(160, textarea.scrollHeight) + 'px';
375
-
376
- // Instant validation
432
+ this.resizeTextarea(textarea);
377
433
  this.validateAndUpdateUI(textarea.value);
378
434
  });
379
435
 
380
- // Validation on paste
436
+
381
437
  textarea.addEventListener('paste', () => {
382
- // Small delay to let paste complete
383
438
  setTimeout(() => {
384
439
  this.validateAndUpdateUI(textarea.value);
440
+ this.resizeTextarea(textarea);
385
441
  }, 10);
386
442
  });
387
443
  }
388
444
 
389
- // Prevent form submission if button is disabled
445
+
390
446
  if (executeBtn) {
391
447
  executeBtn.addEventListener('click', (event) => {
392
448
  if (executeBtn.disabled) {
@@ -403,6 +459,17 @@ document.addEventListener('DOMContentLoaded', function() {
403
459
  window.clearQuery = () => this.clearQuery();
404
460
  window.copyCurrentQuery = () => this.copyCurrentQuery();
405
461
  window.saveCurrentQuery = () => this.saveCurrentQuery();
462
+
463
+ // Listen for form submissions and trigger history refresh
464
+ const form = document.querySelector('.insights-container form');
465
+ if (form) {
466
+ form.addEventListener('submit', () => {
467
+ // Delay to allow the analysis to complete
468
+ setTimeout(() => {
469
+ document.dispatchEvent(new CustomEvent('analysisCompleted'));
470
+ }, 2000);
471
+ });
472
+ }
406
473
 
407
474
  // Table preview dropdown
408
475
  const tableSelect = document.getElementById('table-preview-select');
@@ -0,0 +1,53 @@
1
+ // Enhanced Plan and Performance Views JavaScript - Simplified
2
+ document.addEventListener('DOMContentLoaded', function() {
3
+ const PlanPerformanceEnhancer = {
4
+ init() {
5
+ // Apply score-based CSS classes to any remaining elements
6
+ this.applyScoreClasses();
7
+ },
8
+
9
+ applyScoreClasses() {
10
+ // Find elements that contain score text and apply appropriate classes
11
+ const scoreElements = document.querySelectorAll('.stat-value, .metric-score, .kpi-value');
12
+
13
+ scoreElements.forEach(element => {
14
+ const text = element.textContent.toLowerCase().trim();
15
+ let cssClass = '';
16
+
17
+ switch(text) {
18
+ case 'excellent':
19
+ cssClass = 'score-excellent';
20
+ break;
21
+ case 'good':
22
+ cssClass = 'score-good';
23
+ break;
24
+ case 'fair':
25
+ cssClass = 'score-fair';
26
+ break;
27
+ case 'poor':
28
+ cssClass = 'score-poor';
29
+ break;
30
+ case 'none':
31
+ cssClass = 'score-none';
32
+ break;
33
+ }
34
+
35
+ if (cssClass) {
36
+ element.classList.add(cssClass);
37
+ }
38
+ });
39
+ }
40
+ };
41
+
42
+ // Initialize enhanced views
43
+ PlanPerformanceEnhancer.init();
44
+
45
+ // Re-initialize when views are switched
46
+ document.addEventListener('click', function(e) {
47
+ if (e.target.matches('.toggle-btn[data-view="plan"]')) {
48
+ setTimeout(() => PlanPerformanceEnhancer.applyScoreClasses(), 100);
49
+ } else if (e.target.matches('.toggle-btn[data-view="perf"]')) {
50
+ setTimeout(() => PlanPerformanceEnhancer.applyScoreClasses(), 100);
51
+ }
52
+ });
53
+ });