pg_insights 0.3.2 → 0.4.1

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/pg_insights/application.js +91 -21
  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 -1
  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 +2 -0
  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/engine.rb +8 -0
  39. data/lib/pg_insights/version.rb +1 -1
  40. data/lib/pg_insights.rb +30 -2
  41. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c0047e0a987820049e3fd9eb5b358df1c6a75fa50b5906c601080e3eb8473a5
4
- data.tar.gz: 41c449fb98740eeaf782e0830f28192ef2a85f92a06aa30d268c3d34915d4d83
3
+ metadata.gz: ddf26a0084294d0b53c353cdab1af7754064251af9a395f57364bda181aee532
4
+ data.tar.gz: 8e30fe547411d3a2eb5b67d03dd96637bfb2bee59df96b1a4946a892f0de4e13
5
5
  SHA512:
6
- metadata.gz: f3f5a1cc793a1fae11794e28e347717d55600e7204a6f0fe3530ecc5ddc9412a3685e2d793f2d5224d24d92b1fa0300f743c8019ce5b5c9a362c74b69233831c
7
- data.tar.gz: f92a5dc042459af2a2e6ae00adcbc37d91d99c31c9c62861b54d3a6bd4bd68b3b4ac4dce27500da13acb88b53e772fc39dad937c8ab6df2c754253c29a2493e1
6
+ metadata.gz: f2bce9427d180b8d043536aad436ef7ab73067f389684f4fbbfb18a2cc6ad410dcbf89a074ebb1e7240d73263078d590986593f8728b03373bd66880cd62f72b
7
+ data.tar.gz: 18a82e1a0dc340f3bbeb8c74830f08614bea54bbe351f553274e192562e616d4dc2021f4b6e8d851ab072c55525f9be5367bb430e9aeb33eb69e3d9dd3c69e95
@@ -1,5 +1,4 @@
1
1
 
2
-
3
2
  // PG Insights JavaScript
4
3
  document.addEventListener('DOMContentLoaded', function() {
5
4
  const InsightsApp = {
@@ -23,6 +22,45 @@ document.addEventListener('DOMContentLoaded', function() {
23
22
  this.validateInitialQuery();
24
23
  this.setupQueryExamples();
25
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
+ }
26
64
  },
27
65
 
28
66
  // Load queries from data attribute
@@ -43,7 +81,7 @@ document.addEventListener('DOMContentLoaded', function() {
43
81
  // Keep this for backward compatibility if needed elsewhere
44
82
  },
45
83
 
46
- // Copy current query functionality
84
+
47
85
  copyCurrentQuery() {
48
86
  const textarea = document.querySelector('.sql-editor');
49
87
  const btn = document.querySelector('.btn-icon.btn-copy');
@@ -61,7 +99,7 @@ document.addEventListener('DOMContentLoaded', function() {
61
99
  });
62
100
  },
63
101
 
64
- // Save or Update the current query
102
+
65
103
  saveCurrentQuery() {
66
104
  const textarea = document.querySelector('.sql-editor');
67
105
  const sql = textarea?.value.trim();
@@ -76,7 +114,7 @@ document.addEventListener('DOMContentLoaded', function() {
76
114
  name = prompt("Enter a name for this new saved query:");
77
115
  }
78
116
 
79
- if (!name) return; // User cancelled prompt
117
+ if (!name) return;
80
118
 
81
119
  const method = isUpdate ? 'PATCH' : 'POST';
82
120
  const url = isUpdate ? `/pg_insights/queries/${this.currentQueryState.id}` : '/pg_insights/queries';
@@ -130,7 +168,7 @@ document.addEventListener('DOMContentLoaded', function() {
130
168
  });
131
169
  },
132
170
 
133
- // Query validation
171
+
134
172
  validateQuery(sql) {
135
173
  if (!sql || !sql.trim()) {
136
174
  return { valid: false, message: "Please enter a SQL query" };
@@ -206,7 +244,7 @@ document.addEventListener('DOMContentLoaded', function() {
206
244
  }
207
245
  },
208
246
 
209
- // Query management
247
+
210
248
  clearQuery() {
211
249
  const textarea = document.querySelector('.sql-editor');
212
250
  if (textarea) {
@@ -252,7 +290,7 @@ document.addEventListener('DOMContentLoaded', function() {
252
290
  });
253
291
  },
254
292
 
255
- // Preview selected table
293
+
256
294
  previewTable(tableName) {
257
295
  if (!tableName) return;
258
296
 
@@ -262,12 +300,8 @@ document.addEventListener('DOMContentLoaded', function() {
262
300
  if (textarea) {
263
301
  textarea.value = sql;
264
302
 
265
- // Validate the query
266
303
  this.validateAndUpdateUI(sql);
267
-
268
- // Auto-resize textarea
269
- textarea.style.height = 'auto';
270
- textarea.style.height = Math.max(160, textarea.scrollHeight) + 'px';
304
+ this.resizeTextarea(textarea);
271
305
 
272
306
  // Auto-execute the query
273
307
  const executeBtn = document.getElementById('execute-btn');
@@ -283,6 +317,35 @@ document.addEventListener('DOMContentLoaded', function() {
283
317
  }
284
318
  },
285
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
+
286
349
  loadQueryById(queryId) {
287
350
  const query = this.config.queries.find(q => q.id.toString() === queryId.toString());
288
351
 
@@ -358,7 +421,7 @@ document.addEventListener('DOMContentLoaded', function() {
358
421
  });
359
422
  },
360
423
 
361
- // Event binding
424
+
362
425
  bindEvents() {
363
426
  const textarea = document.querySelector('.sql-editor');
364
427
  const executeBtn = document.getElementById('execute-btn');
@@ -366,24 +429,20 @@ document.addEventListener('DOMContentLoaded', function() {
366
429
  // Real-time validation on input
367
430
  if (textarea) {
368
431
  textarea.addEventListener('input', () => {
369
- // Auto-resize
370
- textarea.style.height = 'auto';
371
- textarea.style.height = Math.max(160, textarea.scrollHeight) + 'px';
372
-
373
- // Instant validation
432
+ this.resizeTextarea(textarea);
374
433
  this.validateAndUpdateUI(textarea.value);
375
434
  });
376
435
 
377
- // Validation on paste
436
+
378
437
  textarea.addEventListener('paste', () => {
379
- // Small delay to let paste complete
380
438
  setTimeout(() => {
381
439
  this.validateAndUpdateUI(textarea.value);
440
+ this.resizeTextarea(textarea);
382
441
  }, 10);
383
442
  });
384
443
  }
385
444
 
386
- // Prevent form submission if button is disabled
445
+
387
446
  if (executeBtn) {
388
447
  executeBtn.addEventListener('click', (event) => {
389
448
  if (executeBtn.disabled) {
@@ -400,6 +459,17 @@ document.addEventListener('DOMContentLoaded', function() {
400
459
  window.clearQuery = () => this.clearQuery();
401
460
  window.copyCurrentQuery = () => this.copyCurrentQuery();
402
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
+ }
403
473
 
404
474
  // Table preview dropdown
405
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
+ });