pg_insights 0.3.2 → 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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/pg_insights/application.js +91 -21
- data/app/assets/javascripts/pg_insights/plan_performance.js +53 -0
- data/app/assets/javascripts/pg_insights/query_comparison.js +1129 -0
- data/app/assets/javascripts/pg_insights/results/view_toggles.js +26 -5
- data/app/assets/javascripts/pg_insights/results.js +231 -1
- data/app/assets/stylesheets/pg_insights/analysis.css +2628 -0
- data/app/assets/stylesheets/pg_insights/application.css +51 -1
- data/app/assets/stylesheets/pg_insights/results.css +12 -1
- data/app/controllers/pg_insights/insights_controller.rb +486 -9
- data/app/helpers/pg_insights/application_helper.rb +339 -0
- data/app/helpers/pg_insights/insights_helper.rb +567 -0
- data/app/jobs/pg_insights/query_analysis_job.rb +142 -0
- data/app/models/pg_insights/query_execution.rb +198 -0
- data/app/services/pg_insights/query_analysis_service.rb +269 -0
- data/app/views/layouts/pg_insights/application.html.erb +2 -0
- data/app/views/pg_insights/insights/_compare_view.html.erb +264 -0
- data/app/views/pg_insights/insights/_empty_state.html.erb +9 -0
- data/app/views/pg_insights/insights/_execution_table_view.html.erb +86 -0
- data/app/views/pg_insights/insights/_history_bar.html.erb +33 -0
- data/app/views/pg_insights/insights/_perf_view.html.erb +244 -0
- data/app/views/pg_insights/insights/_plan_nodes.html.erb +12 -0
- data/app/views/pg_insights/insights/_plan_tree.html.erb +30 -0
- data/app/views/pg_insights/insights/_plan_tree_modern.html.erb +12 -0
- data/app/views/pg_insights/insights/_plan_view.html.erb +159 -0
- data/app/views/pg_insights/insights/_query_panel.html.erb +3 -2
- data/app/views/pg_insights/insights/_result.html.erb +19 -4
- data/app/views/pg_insights/insights/_results_info.html.erb +33 -9
- data/app/views/pg_insights/insights/_results_info_empty.html.erb +10 -0
- data/app/views/pg_insights/insights/_results_panel.html.erb +7 -9
- data/app/views/pg_insights/insights/_results_table.html.erb +0 -5
- data/app/views/pg_insights/insights/_visual_view.html.erb +212 -0
- data/app/views/pg_insights/insights/index.html.erb +4 -1
- data/app/views/pg_insights/timeline/compare.html.erb +3 -3
- data/config/routes.rb +6 -0
- data/lib/generators/pg_insights/install_generator.rb +20 -14
- data/lib/generators/pg_insights/templates/db/migrate/create_pg_insights_query_executions.rb +45 -0
- data/lib/pg_insights/version.rb +1 -1
- data/lib/pg_insights.rb +30 -2
- 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
|
-
|
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
|
-
|
20
|
-
|
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
|
+
});
|