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
@@ -1,25 +1,46 @@
1
1
  function initViewToggles() {
2
- const toggleBtns = document.querySelectorAll('.toggle-btn');
2
+ const toggleBtns = document.querySelectorAll('.toggle-btn[data-view]:not(#compare-tab)');
3
3
  const views = {
4
4
  table: document.getElementById('table-view'),
5
5
  chart: document.getElementById('chart-view'),
6
- stats: document.getElementById('stats-view')
6
+ stats: document.getElementById('stats-view'),
7
+ plan: document.getElementById('plan-view'),
8
+ perf: document.getElementById('perf-view'),
9
+ visual: document.getElementById('visual-view'),
10
+ 'empty-state': document.getElementById('empty-state')
7
11
  };
8
12
 
9
13
  toggleBtns.forEach(function(btn) {
10
14
  btn.addEventListener('click', function() {
11
15
  const targetView = this.dataset.view;
12
16
 
17
+ // Skip if button is disabled
18
+ if (this.classList.contains('disabled')) return;
19
+
13
20
  // Update active button
14
- toggleBtns.forEach(function(b) { b.classList.remove('active'); });
21
+ document.querySelectorAll('.toggle-btn').forEach(function(b) { b.classList.remove('active'); });
15
22
  this.classList.add('active');
16
23
 
17
24
  // Show/hide views
18
25
  Object.keys(views).forEach(function(viewName) {
19
- if (views[viewName]) {
20
- views[viewName].style.display = viewName === targetView ? 'block' : 'none';
26
+ const view = views[viewName];
27
+ if (view) {
28
+ view.style.display = viewName === targetView ? 'block' : 'none';
21
29
  }
22
30
  });
31
+
32
+ // Hide compare view when switching to other views
33
+ const compareView = document.getElementById('compare-view');
34
+ if (compareView && targetView !== 'compare') {
35
+ compareView.style.display = 'none';
36
+ }
37
+
38
+ // Initialize components based on target view
39
+ if (targetView === 'table' && typeof window.tableManager !== 'undefined') {
40
+ window.tableManager.init();
41
+ } else if (targetView === 'visual' && typeof window.initPEV2 !== 'undefined') {
42
+ setTimeout(() => window.initPEV2(), 100);
43
+ }
23
44
  });
24
45
  });
25
46
  }
@@ -9,4 +9,234 @@ document.addEventListener('DOMContentLoaded', function() {
9
9
 
10
10
  function initTableManager() {
11
11
  var tableManager = new TableManager();
12
- }
12
+ }
13
+
14
+ // Initialize performance view enhancements
15
+ function initializePerformanceView() {
16
+ // Apply threshold coloring
17
+ applyThresholdColoring();
18
+
19
+ // Initialize interactive elements
20
+ initializeInteractiveElements();
21
+
22
+ // Apply performance score coloring
23
+ applyPerformanceScores();
24
+ }
25
+
26
+ // Apply threshold indicator coloring
27
+ function applyThresholdColoring() {
28
+ const thresholds = document.querySelectorAll('.metric-threshold');
29
+
30
+ thresholds.forEach(threshold => {
31
+ const text = threshold.textContent.trim();
32
+
33
+ if (text.includes('✓')) {
34
+ threshold.style.background = '#dcfce7';
35
+ threshold.style.color = '#166534';
36
+ } else if (text.includes('⚠')) {
37
+ threshold.style.background = '#fef3c7';
38
+ threshold.style.color = '#92400e';
39
+ }
40
+ });
41
+ }
42
+
43
+ // Initialize interactive elements
44
+ function initializeInteractiveElements() {
45
+ // Add click handlers for recommendation cards
46
+ const recCards = document.querySelectorAll('.recommendation-card');
47
+
48
+ recCards.forEach(card => {
49
+ card.addEventListener('click', function() {
50
+ // Toggle expanded state or copy hint to clipboard
51
+ const hint = this.querySelector('.rec-hint');
52
+ if (hint) {
53
+ navigator.clipboard.writeText(hint.textContent);
54
+ showNotification('SQL hint copied to clipboard');
55
+ }
56
+ });
57
+ });
58
+
59
+ // Add tooltips for performance badges
60
+ const badges = document.querySelectorAll('.analysis-score, .perf-badge');
61
+
62
+ badges.forEach(badge => {
63
+ badge.addEventListener('mouseenter', function() {
64
+ showTooltip(this, getPerformanceExplanation(this.textContent));
65
+ });
66
+
67
+ badge.addEventListener('mouseleave', function() {
68
+ hideTooltip();
69
+ });
70
+ });
71
+ }
72
+
73
+ // Apply performance score coloring with JavaScript
74
+ function applyPerformanceScores() {
75
+ const scores = document.querySelectorAll('.analysis-score');
76
+
77
+ scores.forEach(score => {
78
+ const text = score.textContent.toLowerCase();
79
+ const classes = ['score-excellent', 'score-good', 'score-fair', 'score-poor', 'score-none'];
80
+
81
+ // Remove existing score classes
82
+ score.classList.remove(...classes);
83
+
84
+ // Apply appropriate class based on content
85
+ if (text === 'excellent') {
86
+ score.classList.add('score-excellent');
87
+ } else if (text === 'good') {
88
+ score.classList.add('score-good');
89
+ } else if (text === 'fair') {
90
+ score.classList.add('score-fair');
91
+ } else if (text === 'poor') {
92
+ score.classList.add('score-poor');
93
+ } else {
94
+ score.classList.add('score-none');
95
+ }
96
+ });
97
+ }
98
+
99
+ // Show notification
100
+ function showNotification(message) {
101
+ const notification = document.createElement('div');
102
+ notification.className = 'perf-notification';
103
+ notification.textContent = message;
104
+ notification.style.cssText = `
105
+ position: fixed;
106
+ top: 20px;
107
+ right: 20px;
108
+ background: linear-gradient(135deg, #10b981, #059669);
109
+ color: white;
110
+ padding: 12px 16px;
111
+ border-radius: 8px;
112
+ font-size: 12px;
113
+ font-weight: 600;
114
+ z-index: 10000;
115
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
116
+ opacity: 0;
117
+ transform: translateX(100%);
118
+ transition: all 0.3s ease;
119
+ `;
120
+
121
+ document.body.appendChild(notification);
122
+
123
+ // Animate in
124
+ setTimeout(() => {
125
+ notification.style.opacity = '1';
126
+ notification.style.transform = 'translateX(0)';
127
+ }, 10);
128
+
129
+ // Remove after delay
130
+ setTimeout(() => {
131
+ notification.style.opacity = '0';
132
+ notification.style.transform = 'translateX(100%)';
133
+ setTimeout(() => notification.remove(), 300);
134
+ }, 2000);
135
+ }
136
+
137
+ // Show tooltip
138
+ function showTooltip(element, text) {
139
+ hideTooltip(); // Remove any existing tooltip
140
+
141
+ const tooltip = document.createElement('div');
142
+ tooltip.className = 'perf-tooltip';
143
+ tooltip.textContent = text;
144
+ tooltip.style.cssText = `
145
+ position: absolute;
146
+ background: #1e293b;
147
+ color: white;
148
+ padding: 8px 12px;
149
+ border-radius: 6px;
150
+ font-size: 11px;
151
+ font-weight: 500;
152
+ white-space: nowrap;
153
+ z-index: 10000;
154
+ pointer-events: none;
155
+ opacity: 0;
156
+ transform: translateY(-5px);
157
+ transition: all 0.2s ease;
158
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
159
+ `;
160
+
161
+ document.body.appendChild(tooltip);
162
+
163
+ // Position tooltip
164
+ const rect = element.getBoundingClientRect();
165
+ const tooltipRect = tooltip.getBoundingClientRect();
166
+
167
+ tooltip.style.left = `${rect.left + (rect.width - tooltipRect.width) / 2}px`;
168
+ tooltip.style.top = `${rect.top - tooltipRect.height - 8}px`;
169
+
170
+ // Show tooltip
171
+ setTimeout(() => {
172
+ tooltip.style.opacity = '1';
173
+ tooltip.style.transform = 'translateY(0)';
174
+ }, 10);
175
+ }
176
+
177
+ // Hide tooltip
178
+ function hideTooltip() {
179
+ const existingTooltip = document.querySelector('.perf-tooltip');
180
+ if (existingTooltip) {
181
+ existingTooltip.remove();
182
+ }
183
+ }
184
+
185
+ // Get performance explanation
186
+ function getPerformanceExplanation(scoreText) {
187
+ const explanations = {
188
+ 'excellent': 'Optimal performance with efficient resource usage',
189
+ 'good': 'Good performance with room for minor optimizations',
190
+ 'fair': 'Acceptable performance but optimization recommended',
191
+ 'poor': 'Performance issues detected, optimization needed',
192
+ 'fast execution': 'Query executes within optimal time bounds',
193
+ 'efficient resource usage': 'Memory and CPU usage is well optimized',
194
+ 'good query plan': 'PostgreSQL chose an efficient execution plan'
195
+ };
196
+
197
+ return explanations[scoreText.toLowerCase()] || 'Performance metric indicator';
198
+ }
199
+
200
+ // Enhance timing bars with better animations
201
+ function enhanceTimingBars() {
202
+ const timingBars = document.querySelectorAll('.timing-bar');
203
+
204
+ timingBars.forEach((bar, index) => {
205
+ // Add staggered animation delay
206
+ bar.style.animationDelay = `${index * 0.1}s`;
207
+
208
+ // Add hover effects
209
+ bar.addEventListener('mouseenter', function() {
210
+ this.style.filter = 'brightness(1.1) saturate(1.2)';
211
+ this.style.transform = 'scaleY(1.1)';
212
+ });
213
+
214
+ bar.addEventListener('mouseleave', function() {
215
+ this.style.filter = 'none';
216
+ this.style.transform = 'scaleY(1)';
217
+ });
218
+ });
219
+ }
220
+
221
+ // Initialize when DOM is ready
222
+ document.addEventListener('DOMContentLoaded', function() {
223
+ // Initialize performance view if it exists
224
+ if (document.getElementById('perf-view')) {
225
+ setTimeout(() => {
226
+ initializePerformanceView();
227
+ enhanceTimingBars();
228
+ }, 100);
229
+ }
230
+ });
231
+
232
+ // Re-initialize when perf tab is clicked
233
+ document.addEventListener('click', function(e) {
234
+ if (e.target.closest('[data-tab="perf"]') || e.target.textContent === 'Perf') {
235
+ setTimeout(() => {
236
+ if (document.getElementById('perf-view').style.display !== 'none') {
237
+ initializePerformanceView();
238
+ enhanceTimingBars();
239
+ }
240
+ }, 50);
241
+ }
242
+ });