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.
Files changed (40) 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/version.rb +1 -1
  39. data/lib/pg_insights.rb +30 -2
  40. metadata +20 -2
@@ -0,0 +1,30 @@
1
+ <div class="execution-plan-tree">
2
+ <% if plan_data.present? && plan_data.first %>
3
+ <% plan = plan_data.first['Plan'] %>
4
+ <% if plan %>
5
+ <div class="plan-node-tree">
6
+ <%= render_plan_node(plan, 0) %>
7
+ </div>
8
+ <% end %>
9
+
10
+ <% if plan_data.first['Planning Time'] || plan_data.first['Execution Time'] %>
11
+ <div class="plan-timing">
12
+ <% if plan_data.first['Planning Time'] %>
13
+ <div class="timing-detail">Planning Time: <%= plan_data.first['Planning Time'] %> ms</div>
14
+ <% end %>
15
+ <% if plan_data.first['Execution Time'] %>
16
+ <div class="timing-detail">Execution Time: <%= plan_data.first['Execution Time'] %> ms</div>
17
+ <% end %>
18
+ </div>
19
+ <% end %>
20
+ <% else %>
21
+ <div class="plan-error">
22
+ <p>Unable to parse execution plan data</p>
23
+ <% if plan_data.is_a?(Hash) && plan_data['error'] %>
24
+ <div class="error-detail"><%= plan_data['error'] %></div>
25
+ <% end %>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+
30
+
@@ -0,0 +1,12 @@
1
+ <div class="plan-tree-modern">
2
+ <% if plan_data.is_a?(Array) && plan_data.first&.dig("Plan") %>
3
+ <%= render_plan_node_modern(plan_data.first["Plan"], 0) %>
4
+ <% elsif plan_data.is_a?(Hash) && plan_data["Plan"] %>
5
+ <%= render_plan_node_modern(plan_data["Plan"], 0) %>
6
+ <% else %>
7
+ <div class="plan-error">
8
+ <span class="error-icon">⚠️</span>
9
+ <span>Unable to parse execution plan data</span>
10
+ </div>
11
+ <% end %>
12
+ </div>
@@ -0,0 +1,159 @@
1
+ <div id="plan-view" class="view-content"<%= ' style="display: none;"'.html_safe unless @execution_type == 'analyze' && !@result.present? %>>
2
+ <div class="analysis-dashboard">
3
+ <!-- Enhanced Plan Header with Rich Metrics -->
4
+ <div class="analysis-header">
5
+ <div class="metric-chips">
6
+ <div class="chip primary">
7
+ <span class="chip-label">Cost</span>
8
+ <span class="chip-value"><%= execution.query_cost&.round(1) || 'N/A' %></span>
9
+ </div>
10
+ <div class="chip success">
11
+ <span class="chip-label">Time</span>
12
+ <span class="chip-value"><%= execution.execution_time_ms ? "#{execution.execution_time_ms.round(1)}ms" : 'N/A' %></span>
13
+ </div>
14
+ <div class="chip info">
15
+ <span class="chip-label">Planning</span>
16
+ <span class="chip-value"><%= execution.planning_time_ms ? "#{execution.planning_time_ms.round(1)}ms" : 'N/A' %></span>
17
+ </div>
18
+ <% if execution.execution_plan.present? %>
19
+ <% plan_metrics = extract_rich_plan_metrics(execution) %>
20
+ <% if plan_metrics[:rows_returned] %>
21
+ <div class="chip secondary">
22
+ <span class="chip-label">Rows</span>
23
+ <span class="chip-value"><%= plan_metrics[:rows_returned] %></span>
24
+ </div>
25
+ <% end %>
26
+ <% if plan_metrics[:workers_launched] && plan_metrics[:workers_launched] > 0 %>
27
+ <div class="chip accent">
28
+ <span class="chip-label">Workers</span>
29
+ <span class="chip-value"><%= plan_metrics[:workers_launched] %></span>
30
+ </div>
31
+ <% end %>
32
+ <% if plan_metrics[:memory_usage_kb] && plan_metrics[:memory_usage_kb] > 0 %>
33
+ <div class="chip warning">
34
+ <span class="chip-label">Memory</span>
35
+ <span class="chip-value"><%= "#{plan_metrics[:memory_usage_kb]}KB" %></span>
36
+ </div>
37
+ <% end %>
38
+ <% end %>
39
+ </div>
40
+ <div class="execution-summary">
41
+ <span class="summary-text"><%= execution.plan_summary %></span>
42
+ </div>
43
+ </div>
44
+
45
+ <% if execution.execution_plan.present? %>
46
+ <!-- Rich Plan Analysis -->
47
+ <div class="plan-analysis-section">
48
+ <h4 class="section-title">Plan Analysis</h4>
49
+ <div class="plan-stats-grid" data-execution-id="<%= execution.id %>">
50
+ <% plan_metrics = extract_rich_plan_metrics(execution) %>
51
+ <div class="stat-card">
52
+ <div class="stat-content">
53
+ <div class="stat-value" id="plan-nodes-count"><%= plan_metrics[:node_count] || 'N/A' %></div>
54
+ <div class="stat-label">Plan Nodes</div>
55
+ </div>
56
+ </div>
57
+ <div class="stat-card">
58
+ <div class="stat-content">
59
+ <div class="stat-value" id="plan-scan-types"><%= plan_metrics[:scan_types]&.any? ? plan_metrics[:scan_types].join(', ') : 'N/A' %></div>
60
+ <div class="stat-label">Scan Types</div>
61
+ </div>
62
+ </div>
63
+ <div class="stat-card">
64
+ <div class="stat-content">
65
+ <div class="stat-value" id="plan-indexes-used"><%= plan_metrics[:index_usage]&.any? ? "#{plan_metrics[:index_usage].length} indexes" : 'None' %></div>
66
+ <div class="stat-label">Indexes Used</div>
67
+ </div>
68
+ </div>
69
+ <div class="stat-card">
70
+ <div class="stat-content">
71
+ <div class="stat-value" id="plan-efficiency" class="<%= score_css_class(calculate_plan_efficiency_score(plan_metrics)) %>"><%= calculate_plan_efficiency_score(plan_metrics) %></div>
72
+ <div class="stat-label">Efficiency</div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Detailed Plan Breakdown -->
79
+ <div class="plan-breakdown-section">
80
+ <h4 class="section-title">Execution Details</h4>
81
+ <div class="plan-details-table" id="plan-details-table">
82
+ <div class="details-grid">
83
+ <div class="detail-item">
84
+ <span class="detail-label">Total Time</span>
85
+ <span class="detail-value"><%= execution.total_time_ms ? "#{execution.total_time_ms.round(1)}ms" : 'N/A' %></span>
86
+ </div>
87
+ <div class="detail-item">
88
+ <span class="detail-label">Planning Time</span>
89
+ <span class="detail-value"><%= execution.planning_time_ms ? "#{execution.planning_time_ms.round(1)}ms" : 'N/A' %></span>
90
+ </div>
91
+ <div class="detail-item">
92
+ <span class="detail-label">Execution Time</span>
93
+ <span class="detail-value"><%= execution.execution_time_ms ? "#{execution.execution_time_ms.round(1)}ms" : 'N/A' %></span>
94
+ </div>
95
+ <div class="detail-item">
96
+ <span class="detail-label">Rows Returned</span>
97
+ <span class="detail-value"><%= plan_metrics[:rows_returned] || 'N/A' %></span>
98
+ </div>
99
+ <div class="detail-item">
100
+ <span class="detail-label">Rows Scanned</span>
101
+ <span class="detail-value"><%= plan_metrics[:rows_scanned] ? number_with_delimiter(plan_metrics[:rows_scanned]) : 'N/A' %></span>
102
+ </div>
103
+ <% if plan_metrics[:memory_usage_kb] && plan_metrics[:memory_usage_kb] > 0 %>
104
+ <div class="detail-item">
105
+ <span class="detail-label">Peak Memory</span>
106
+ <span class="detail-value"><%= "#{plan_metrics[:memory_usage_kb]} KB" %></span>
107
+ </div>
108
+ <% end %>
109
+ <% if plan_metrics[:workers_launched] && plan_metrics[:workers_launched] > 0 %>
110
+ <div class="detail-item">
111
+ <span class="detail-label">Workers</span>
112
+ <span class="detail-value"><%= "#{plan_metrics[:workers_launched]}/#{plan_metrics[:workers_planned]}" %></span>
113
+ </div>
114
+ <% end %>
115
+ <% if plan_metrics[:sort_methods]&.any? %>
116
+ <div class="detail-item">
117
+ <span class="detail-label">Sort Methods</span>
118
+ <span class="detail-value"><%= plan_metrics[:sort_methods].join(', ') %></span>
119
+ </div>
120
+ <% end %>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ <!-- Performance Issues & Recommendations -->
126
+ <div class="plan-insights-section">
127
+ <h4 class="section-title">Insights & Recommendations</h4>
128
+ <div class="plan-insights-grid">
129
+ <div class="insights-column">
130
+ <h5>🚨 Performance Issues</h5>
131
+ <ul id="plan-performance-issues" class="insight-list">
132
+ <%= render_plan_performance_issues(plan_metrics, execution) %>
133
+ </ul>
134
+ </div>
135
+ <div class="insights-column">
136
+ <h5>🔧 Optimization Opportunities</h5>
137
+ <ul id="plan-optimizations" class="insight-list">
138
+ <%= render_plan_optimizations(plan_metrics, execution) %>
139
+ </ul>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Visual Execution Plan -->
145
+ <div class="plan-visualization">
146
+ <h4 class="section-title">Visual Plan Tree</h4>
147
+ <div class="plan-flow">
148
+ <%= render "plan_nodes", plan_data: execution.execution_plan %>
149
+ </div>
150
+ </div>
151
+ <% else %>
152
+ <div class="analysis-empty">
153
+ <div class="empty-icon">🔍</div>
154
+ <div class="empty-message">No execution plan available</div>
155
+ <div class="empty-hint">Use "Analyze" button to generate execution plan</div>
156
+ </div>
157
+ <% end %>
158
+ </div>
159
+ </div>
@@ -22,14 +22,15 @@
22
22
  </div>
23
23
 
24
24
  <div class="form-actions">
25
- <%= f.submit "Execute", class: "btn btn-primary", id: "execute-btn" %>
25
+ <%= f.submit "Execute", class: "btn btn-primary", id: "execute-btn", name: "execute_button" %>
26
+ <%= f.submit "Analyze", class: "btn btn-info", id: "analyze-btn", name: "analyze_button" %>
26
27
  <button type="button" class="btn btn-secondary" onclick="clearQuery()">Clear</button>
27
28
  </div>
28
29
 
29
30
  <%= render "query_examples" %>
30
31
 
31
32
  <div class="query-info">
32
- <small>SELECT only • 5s timeout • 1k row limit</small>
33
+ <small><%= query_info_text %></small>
33
34
  </div>
34
35
  <% end %>
35
36
  </div>
@@ -1,9 +1,24 @@
1
- <% if @result.present? %>
1
+ <% if @result.present? || @query_execution&.success? %>
2
2
  <div class="results-section">
3
3
  <%= render "results_info" %>
4
- <%= render "chart_view" %>
5
- <%= render "stats_view" %>
6
- <%= render "table_view" %>
4
+
5
+ <% if @result.present? %>
6
+ <%= render "chart_view" %>
7
+ <%= render "stats_view" %>
8
+ <%= render "table_view" %>
9
+ <% end %>
10
+
11
+ <% if @query_execution&.includes_execution? && @query_execution.has_result_data? %>
12
+ <%= render "execution_table_view", execution: @query_execution %>
13
+ <% end %>
14
+
15
+ <% if @query_execution&.has_plan_data? %>
16
+ <%= render "plan_view", execution: @query_execution %>
17
+ <%= render "perf_view", execution: @query_execution %>
18
+ <%= render "visual_view", execution: @query_execution %>
19
+ <% end %>
20
+
21
+ <%= render "compare_view" %>
7
22
  </div>
8
23
  <% elsif flash.now[:alert] %>
9
24
  <div class="results-section">
@@ -1,19 +1,43 @@
1
1
  <div class="results-info">
2
2
  <div class="results-meta">
3
- <%= "#{@result.rows.size} records • #{@result.columns.size} columns" if @result %>
3
+ <% if @result.present? %>
4
+ <%= "#{@result.rows.size} records • #{@result.columns.size} columns" %>
5
+ <% elsif @query_execution&.success? && @query_execution.has_result_data? %>
6
+ <%= @query_execution.result_summary %>
7
+ <% elsif @query_execution&.success? && @query_execution.has_plan_data? %>
8
+ Analysis completed • <%= @query_execution.formatted_total_time %>
9
+ <% end %>
4
10
  </div>
5
11
 
6
12
  <div class="view-toggle">
7
- <button type="button" class="toggle-btn active" data-view="table">
8
- <span class="toggle-icon">📊</span> Table
9
- </button>
10
- <% if should_show_chart?(@result) %>
11
- <button type="button" class="toggle-btn" data-view="chart">
12
- <span class="toggle-icon">📈</span> Chart
13
+ <% if @result.present? || (@query_execution&.includes_execution? && @query_execution.has_result_data?) %>
14
+ <button type="button" class="toggle-btn<%= ' active' if @result.present? || (@query_execution&.includes_execution? && !@query_execution.has_plan_data?) %>" data-view="table">
15
+ <span class="toggle-icon">📊</span> Table
16
+ </button>
17
+ <% if @result && should_show_chart?(@result) %>
18
+ <button type="button" class="toggle-btn" data-view="chart">
19
+ <span class="toggle-icon">📈</span> Chart
20
+ </button>
21
+ <% end %>
22
+ <button type="button" class="toggle-btn" data-view="stats">
23
+ <span class="toggle-icon">📋</span> Stats
24
+ </button>
25
+ <% end %>
26
+
27
+ <% if @query_execution&.has_plan_data? %>
28
+ <button type="button" class="toggle-btn<%= ' active' if @execution_type == 'analyze' && !(@result.present?) %>" data-view="plan">
29
+ <span class="toggle-icon">🔍</span> Plan
30
+ </button>
31
+ <button type="button" class="toggle-btn" data-view="perf">
32
+ <span class="toggle-icon">⏱️</span> Perf
33
+ </button>
34
+ <button type="button" class="toggle-btn" data-view="visual">
35
+ <span class="toggle-icon">🌳</span> Visual
13
36
  </button>
14
37
  <% end %>
15
- <button type="button" class="toggle-btn" data-view="stats">
16
- <span class="toggle-icon">📋</span> Stats
38
+
39
+ <button type="button" id="compare-tab" class="toggle-btn disabled" data-view="compare" title="Select 2 queries from history to compare">
40
+ <span class="toggle-icon">⚖️</span> Compare
17
41
  </button>
18
42
  </div>
19
43
  </div>
@@ -0,0 +1,10 @@
1
+ <div class="results-info">
2
+ <div class="results-meta">
3
+ <span>Ready to Query</span>
4
+ </div>
5
+ <div class="view-toggle">
6
+ <button type="button" id="compare-tab" class="toggle-btn disabled" data-view="compare" title="Select 2 queries from history to compare">
7
+ <span class="toggle-icon">⚖️</span> Compare
8
+ </button>
9
+ </div>
10
+ </div>
@@ -1,13 +1,11 @@
1
1
  <div class="results-panel">
2
- <% if @result.nil? %>
3
- <div class="empty-results">
4
- <div class="empty-content">
5
- <div class="empty-icon">📊</div>
6
- <h3>Ready to Query</h3>
7
- <p>Enter a SELECT query on the left and click Execute to see results here.</p>
8
- </div>
9
- </div>
10
- <% else %>
2
+ <% if @result.present? || @query_execution&.success? %>
11
3
  <%= render "result" %>
4
+ <% else %>
5
+ <div class="results-section">
6
+ <%= render "results_info_empty" %>
7
+ <%= render "empty_state" %>
8
+ <%= render "compare_view" %>
9
+ </div>
12
10
  <% end %>
13
11
  </div>
@@ -1,7 +1,6 @@
1
1
  <div class="table-wrapper">
2
2
  <div id="tableScroll" class="table-scroll" tabindex="0">
3
3
  <table id="resultsTable" class="results-table">
4
- <!-- Sticky Header -->
5
4
  <thead class="sticky-header">
6
5
  <tr>
7
6
  <th class="row-num">#</th>
@@ -15,8 +14,6 @@
15
14
  <% end %>
16
15
  </tr>
17
16
  </thead>
18
-
19
- <!-- Table Body -->
20
17
  <tbody>
21
18
  <% @result.rows.each_with_index do |row, row_index| %>
22
19
  <tr class="<%= 'even-row' if row_index.even? %>">
@@ -33,8 +30,6 @@
33
30
  </tbody>
34
31
  </table>
35
32
  </div>
36
-
37
- <!-- Scroll Indicators -->
38
33
  <div id="scrollIndicatorH" class="scroll-indicator scroll-indicator-horizontal">
39
34
  <div id="scrollThumbH" class="scroll-thumb"></div>
40
35
  </div>
@@ -0,0 +1,212 @@
1
+ <div id="visual-view" class="view-content" style="display: none;">
2
+ <div class="visual-plan-container">
3
+ <!-- PEV2 Vue App Container with scoped Bootstrap -->
4
+ <div id="pev2-app" class="pev2-bootstrap-scope">
5
+ <div class="loading-container">
6
+ <div class="loading-spinner"></div>
7
+ <p class="loading-text">Loading execution plan visualization...</p>
8
+ </div>
9
+ </div>
10
+ </div>
11
+
12
+ <script>
13
+ // Dynamic dependency loader
14
+ window.loadPEV2Dependencies = function() {
15
+ return new Promise((resolve, reject) => {
16
+ // Check if already loaded
17
+ if (window.pev2DependenciesLoaded) {
18
+ resolve();
19
+ return;
20
+ }
21
+
22
+ let loadedCount = 0;
23
+ const totalDependencies = 3; // CSS, Vue, PEV2
24
+
25
+ function checkComplete() {
26
+ loadedCount++;
27
+ if (loadedCount === totalDependencies) {
28
+ window.pev2DependenciesLoaded = true;
29
+ resolve();
30
+ }
31
+ }
32
+
33
+ // Load Bootstrap CSS with scoping
34
+ const bootstrapCSS = document.createElement('link');
35
+ bootstrapCSS.rel = 'stylesheet';
36
+ bootstrapCSS.href = 'https://unpkg.com/bootstrap@5.3.2/dist/css/bootstrap.min.css';
37
+ bootstrapCSS.onload = () => {
38
+ // Add PEV2 CSS
39
+ const pev2CSS = document.createElement('link');
40
+ pev2CSS.rel = 'stylesheet';
41
+ pev2CSS.href = 'https://unpkg.com/pev2/dist/pev2.css';
42
+ pev2CSS.onload = checkComplete;
43
+ pev2CSS.onerror = reject;
44
+ document.head.appendChild(pev2CSS);
45
+ };
46
+ bootstrapCSS.onerror = reject;
47
+ document.head.appendChild(bootstrapCSS);
48
+
49
+ // Load Vue.js
50
+ const vueScript = document.createElement('script');
51
+ vueScript.src = 'https://unpkg.com/vue@3.2.45/dist/vue.global.prod.js';
52
+ vueScript.onload = checkComplete;
53
+ vueScript.onerror = reject;
54
+ document.head.appendChild(vueScript);
55
+
56
+ // Load PEV2
57
+ const pev2Script = document.createElement('script');
58
+ pev2Script.src = 'https://unpkg.com/pev2/dist/pev2.umd.js';
59
+ pev2Script.onload = checkComplete;
60
+ pev2Script.onerror = reject;
61
+ document.head.appendChild(pev2Script);
62
+ });
63
+ };
64
+
65
+ // Initialize PEV2 after dependencies are loaded
66
+ window.initPEV2 = function() {
67
+ if (window.pev2Initialized) {
68
+ return; // Already initialized
69
+ }
70
+
71
+ // Show loading state
72
+ document.getElementById('pev2-app').innerHTML = `
73
+ <div class="d-flex align-items-center justify-content-center" style="height: 500px;">
74
+ <div class="text-center">
75
+ <div class="spinner-border text-primary mb-3" role="status">
76
+ <span class="visually-hidden">Loading...</span>
77
+ </div>
78
+ <p class="text-muted">Loading dependencies...</p>
79
+ </div>
80
+ </div>
81
+ `;
82
+
83
+ // Load dependencies dynamically
84
+ window.loadPEV2Dependencies()
85
+ .then(() => {
86
+ if (!window.Vue || !window.pev2) {
87
+ throw new Error('Dependencies failed to load properly');
88
+ }
89
+
90
+ const { createApp } = Vue;
91
+
92
+ // Get the execution plan data as JavaScript object
93
+ const executionData = <%= raw(@query_execution.execution_plan.to_json) if @query_execution&.execution_plan.present? %> || null;
94
+ const queryText = <%= raw(@query_execution.sql_text.to_json) if @query_execution&.sql_text.present? %> || '';
95
+
96
+ // PEV2 expects the plan as a JSON string
97
+ let planSource = '{}';
98
+ if (executionData && Array.isArray(executionData) && executionData.length > 0) {
99
+ planSource = JSON.stringify(executionData[0]);
100
+ } else if (executionData && typeof executionData === 'object') {
101
+ planSource = JSON.stringify(executionData);
102
+ }
103
+
104
+ console.log('PEV2 Plan Source:', planSource);
105
+
106
+ // Create Vue app with scoped container
107
+ const app = createApp({
108
+ data() {
109
+ return {
110
+ plan: planSource,
111
+ queryText: queryText
112
+ }
113
+ },
114
+ template: `
115
+ <div class="d-flex flex-column vh-100">
116
+ <pev2 :plan-source="plan" :plan-query="queryText" />
117
+ </div>
118
+ `
119
+ });
120
+
121
+ app.component('pev2', pev2.Plan);
122
+ app.mount('#pev2-app');
123
+ window.pev2Initialized = true;
124
+
125
+ })
126
+ .catch(error => {
127
+ console.error('Error loading PEV2 dependencies:', error);
128
+ document.getElementById('pev2-app').innerHTML = `
129
+ <div class="alert alert-danger m-4">
130
+ <h5>❌ Failed to load execution plan visualization</h5>
131
+ <p>Could not load the required dependencies (Vue.js, PEV2). This could be due to:</p>
132
+ <ul>
133
+ <li>Network connectivity issues</li>
134
+ <li>CDN service unavailable</li>
135
+ <li>Browser security settings</li>
136
+ </ul>
137
+ <p><strong>Alternative:</strong> Use the <strong>Plan</strong> or <strong>Perf</strong> tabs for execution plan analysis.</p>
138
+ <details>
139
+ <summary>Technical Details</summary>
140
+ <code>${error.message}</code>
141
+ </details>
142
+ </div>
143
+ `;
144
+ });
145
+ };
146
+ </script>
147
+
148
+ <!-- Scoped CSS to prevent Bootstrap conflicts -->
149
+ <style>
150
+ /* Scope Bootstrap styles to only affect PEV2 container */
151
+ .pev2-bootstrap-scope {
152
+ /* Isolate Bootstrap styles */
153
+ all: initial;
154
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
155
+ }
156
+
157
+ .pev2-bootstrap-scope * {
158
+ box-sizing: border-box;
159
+ }
160
+
161
+ /* Custom loading styles */
162
+ .loading-container {
163
+ display: flex;
164
+ flex-direction: column;
165
+ align-items: center;
166
+ justify-content: center;
167
+ height: 500px;
168
+ background: #f8f9fa;
169
+ border: 1px solid #dee2e6;
170
+ border-radius: 8px;
171
+ margin: 16px;
172
+ }
173
+
174
+ .loading-spinner {
175
+ width: 40px;
176
+ height: 40px;
177
+ border: 4px solid #e9ecef;
178
+ border-left: 4px solid #007bff;
179
+ border-radius: 50%;
180
+ animation: spin 1s linear infinite;
181
+ margin-bottom: 16px;
182
+ }
183
+
184
+ @keyframes spin {
185
+ 0% { transform: rotate(0deg); }
186
+ 100% { transform: rotate(360deg); }
187
+ }
188
+
189
+ .loading-text {
190
+ color: #6c757d;
191
+ font-size: 14px;
192
+ margin: 0;
193
+ }
194
+
195
+ /* Ensure visual view takes full height */
196
+ #visual-view {
197
+ padding: 0;
198
+ }
199
+
200
+ .visual-plan-container {
201
+ width: 100%;
202
+ height: 100%;
203
+ min-height: 500px;
204
+ }
205
+
206
+ #pev2-app {
207
+ width: 100%;
208
+ height: 100%;
209
+ background: white;
210
+ }
211
+ </style>
212
+ </div>
@@ -1,5 +1,8 @@
1
1
 
2
2
  <div class="insights-container" data-queries="<%= @insight_queries.to_json %>">
3
3
  <%= render "query_panel" %>
4
- <%= render "results_panel" %>
4
+ <div class="results-section-wrapper">
5
+ <%= render "history_bar" %>
6
+ <%= render "results_panel" %>
7
+ </div>
5
8
  </div>
@@ -120,7 +120,7 @@
120
120
  <div class="panel-header">
121
121
  <h3>📊 Key Metrics</h3>
122
122
  </div>
123
- <div class="metrics-table">
123
+ <div class="timeline-metrics-table">
124
124
  <% if @performance_comparison && @performance_comparison[:metrics] %>
125
125
  <% @performance_comparison[:metrics].each do |metric, data| %>
126
126
  <div class="metric-row">
@@ -529,8 +529,8 @@
529
529
  border-radius: 4px;
530
530
  }
531
531
 
532
- /* Metrics Table */
533
- .metrics-table {
532
+ /* Timeline Metrics Table */
533
+ .timeline-metrics-table {
534
534
  padding: var(--space-md);
535
535
  }
536
536
 
data/config/routes.rb CHANGED
@@ -4,6 +4,12 @@ PgInsights::Engine.routes.draw do
4
4
  post "/", to: "insights#index"
5
5
  get :table_names, to: "insights#table_names"
6
6
 
7
+ # Query analysis endpoints
8
+ post :analyze, to: "insights#analyze"
9
+ get "execution/:id", to: "insights#execution_status", as: :execution_status
10
+ get :query_history, to: "insights#query_history", defaults: { format: :json }
11
+ post :compare, to: "insights#compare", defaults: { format: :json }
12
+
7
13
  resources :queries, only: [ :create, :update, :destroy ]
8
14
 
9
15
  get :health, to: "health#index"