rails_db_inspector 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +232 -0
- data/Rakefile +3 -0
- data/app/assets/stylesheets/rails_db_inspector/application.css +41 -0
- data/app/controllers/rails_db_inspector/application_controller.rb +15 -0
- data/app/controllers/rails_db_inspector/queries_controller.rb +42 -0
- data/app/controllers/rails_db_inspector/schema_controller.rb +13 -0
- data/app/helpers/rails_db_inspector/application_helper.rb +274 -0
- data/app/helpers/rails_db_inspector/plan_renderer.rb +887 -0
- data/app/jobs/rails_db_inspector/application_job.rb +4 -0
- data/app/mailers/rails_db_inspector/application_mailer.rb +6 -0
- data/app/models/rails_db_inspector/application_record.rb +5 -0
- data/app/views/layouts/rails_db_inspector/application.html.erb +55 -0
- data/app/views/rails_db_inspector/queries/explain.html.erb +128 -0
- data/app/views/rails_db_inspector/queries/index.html.erb +258 -0
- data/app/views/rails_db_inspector/queries/show.html.erb +103 -0
- data/app/views/rails_db_inspector/schema/index.html.erb +842 -0
- data/config/routes.rb +17 -0
- data/lib/rails_db_inspector/configuration.rb +17 -0
- data/lib/rails_db_inspector/dev_widget_middleware.rb +145 -0
- data/lib/rails_db_inspector/engine.rb +22 -0
- data/lib/rails_db_inspector/explain/my_sql.rb +28 -0
- data/lib/rails_db_inspector/explain/postgres.rb +32 -0
- data/lib/rails_db_inspector/explain.rb +27 -0
- data/lib/rails_db_inspector/query_store.rb +89 -0
- data/lib/rails_db_inspector/schema_inspector.rb +222 -0
- data/lib/rails_db_inspector/sql_subscriber.rb +42 -0
- data/lib/rails_db_inspector/version.rb +3 -0
- data/lib/rails_db_inspector.rb +25 -0
- data/lib/tasks/rails_db_inspector_tasks.rake +4 -0
- metadata +91 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Rails DB Inspector</title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<%= csp_meta_tag %>
|
|
9
|
+
|
|
10
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
11
|
+
<script>
|
|
12
|
+
tailwind.config = {
|
|
13
|
+
theme: {
|
|
14
|
+
extend: {
|
|
15
|
+
fontFamily: {
|
|
16
|
+
'mono': ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<%= yield :head %>
|
|
24
|
+
</head>
|
|
25
|
+
<body class="h-full bg-gray-50">
|
|
26
|
+
<div class="min-h-full">
|
|
27
|
+
<!-- Header -->
|
|
28
|
+
<header class="bg-white shadow-sm border-b border-gray-200">
|
|
29
|
+
<div class="mx-auto px-4 sm:px-6 lg:px-8">
|
|
30
|
+
<div class="flex h-16 justify-between items-center">
|
|
31
|
+
<div class="flex items-center space-x-4">
|
|
32
|
+
<h1 class="text-xl font-semibold text-gray-900">
|
|
33
|
+
<%= link_to "Rails DB Inspector", queries_path, class: "hover:text-blue-600 transition-colors" %>
|
|
34
|
+
</h1>
|
|
35
|
+
<nav class="flex items-center space-x-3 ml-6">
|
|
36
|
+
<%= link_to "Queries", queries_path, class: "text-sm font-medium #{'text-blue-600' if controller_name == 'queries'} #{'text-gray-500 hover:text-gray-700' unless controller_name == 'queries'} transition-colors" %>
|
|
37
|
+
<%= link_to "Schema", schema_index_path, class: "text-sm font-medium #{'text-blue-600' if controller_name == 'schema'} #{'text-gray-500 hover:text-gray-700' unless controller_name == 'schema'} transition-colors" %>
|
|
38
|
+
</nav>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="flex items-center space-x-4">
|
|
41
|
+
<span class="text-sm text-gray-500">Query Analysis & Performance Monitoring</span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</header>
|
|
46
|
+
|
|
47
|
+
<!-- Main Content -->
|
|
48
|
+
<main class="mx-auto px-4 py-6 sm:px-6 lg:px-8">
|
|
49
|
+
<%= yield %>
|
|
50
|
+
</main>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<%= yield :scripts %>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<% content_for :scripts do %>
|
|
2
|
+
<script>
|
|
3
|
+
// Plan Tree JavaScript
|
|
4
|
+
function toggleNode(nodeId) {
|
|
5
|
+
const nodeBody = document.getElementById(nodeId);
|
|
6
|
+
const header = nodeBody.previousElementSibling;
|
|
7
|
+
const toggle = header.querySelector('.expand-toggle');
|
|
8
|
+
|
|
9
|
+
if (nodeBody.style.display === 'none') {
|
|
10
|
+
nodeBody.style.display = 'block';
|
|
11
|
+
header.classList.remove('collapsed');
|
|
12
|
+
if (toggle) toggle.textContent = '−';
|
|
13
|
+
} else {
|
|
14
|
+
nodeBody.style.display = 'none';
|
|
15
|
+
header.classList.add('collapsed');
|
|
16
|
+
if (toggle) toggle.textContent = '+';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Initialize collapsed state for large trees
|
|
21
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
22
|
+
const allNodes = document.querySelectorAll('.plan-node');
|
|
23
|
+
|
|
24
|
+
// Auto-collapse nodes beyond depth 2 if there are many nodes
|
|
25
|
+
if (allNodes.length > 10) {
|
|
26
|
+
let depth = 0;
|
|
27
|
+
function collapseDeepNodes(element) {
|
|
28
|
+
if (element.classList.contains('plan-node')) {
|
|
29
|
+
depth++;
|
|
30
|
+
if (depth > 2) {
|
|
31
|
+
const header = element.querySelector('.plan-node-header');
|
|
32
|
+
const body = element.querySelector('.plan-node-body');
|
|
33
|
+
const toggle = header.querySelector('.expand-toggle');
|
|
34
|
+
|
|
35
|
+
if (body) {
|
|
36
|
+
body.style.display = 'none';
|
|
37
|
+
header.classList.add('collapsed');
|
|
38
|
+
if (toggle) toggle.textContent = '+';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Process children
|
|
43
|
+
const children = element.querySelector('.plan-node-children');
|
|
44
|
+
if (children) {
|
|
45
|
+
for (let child of children.children) {
|
|
46
|
+
collapseDeepNodes(child);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
depth--;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rootNode = document.querySelector('.plan-node');
|
|
55
|
+
if (rootNode) {
|
|
56
|
+
collapseDeepNodes(rootNode);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
</script>
|
|
61
|
+
<% end %>
|
|
62
|
+
|
|
63
|
+
<div class="space-y-6">
|
|
64
|
+
<!-- Header -->
|
|
65
|
+
<div class="flex items-center justify-between">
|
|
66
|
+
<div>
|
|
67
|
+
<h1 class="text-3xl font-bold text-gray-900">Query Execution Plan</h1>
|
|
68
|
+
<p class="mt-2 text-gray-600">Detailed analysis of query performance and execution strategy</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div>
|
|
71
|
+
<%= link_to "← Back to Query", query_path(@query.id),
|
|
72
|
+
class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors" %>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- SQL Query -->
|
|
77
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
78
|
+
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
|
79
|
+
<h2 class="text-lg font-medium text-gray-900">SQL Query</h2>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="p-6">
|
|
82
|
+
<div class="bg-gray-900 rounded-md p-4 overflow-x-auto">
|
|
83
|
+
<pre class="text-green-400 text-sm font-mono whitespace-pre-wrap"><%= @query.sql %></pre>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<!-- Execution Plan -->
|
|
89
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
90
|
+
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
|
91
|
+
<h2 class="text-lg font-medium text-gray-900">Execution Plan</h2>
|
|
92
|
+
<p class="mt-1 text-sm text-gray-500">
|
|
93
|
+
<% if @explain[:analyze] %>
|
|
94
|
+
Live execution analysis with actual timing and row counts
|
|
95
|
+
<% else %>
|
|
96
|
+
Estimated execution plan (run with ANALYZE for actual metrics)
|
|
97
|
+
<% end %>
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="p-6">
|
|
101
|
+
<% if @explain[:adapter] == "postgres" %>
|
|
102
|
+
<%= render_postgres_plan(@explain) %>
|
|
103
|
+
<% else %>
|
|
104
|
+
<!-- MySQL/Other adapters -->
|
|
105
|
+
<div class="overflow-x-auto">
|
|
106
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
107
|
+
<thead class="bg-gray-50">
|
|
108
|
+
<tr>
|
|
109
|
+
<% @explain[:columns].each do |c| %>
|
|
110
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"><%= c %></th>
|
|
111
|
+
<% end %>
|
|
112
|
+
</tr>
|
|
113
|
+
</thead>
|
|
114
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
115
|
+
<% @explain[:rows].each_with_index do |row, index| %>
|
|
116
|
+
<tr class="<%= 'bg-gray-50' if index.even? %>">
|
|
117
|
+
<% row.each do |cell| %>
|
|
118
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-mono"><%= cell %></td>
|
|
119
|
+
<% end %>
|
|
120
|
+
</tr>
|
|
121
|
+
<% end %>
|
|
122
|
+
</tbody>
|
|
123
|
+
</table>
|
|
124
|
+
</div>
|
|
125
|
+
<% end %>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<div class="space-y-6">
|
|
2
|
+
<!-- Header Section -->
|
|
3
|
+
<div class="flex justify-between items-center">
|
|
4
|
+
<div>
|
|
5
|
+
<h1 class="text-3xl font-bold text-gray-900">Query Monitor</h1>
|
|
6
|
+
<p class="mt-2 text-gray-600">Real-time SQL query capture and performance analysis</p>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="flex space-x-3">
|
|
9
|
+
<%= button_to "Clear All", clear_queries_path, method: :post,
|
|
10
|
+
class: "inline-flex items-center px-4 py-2 border border-red-300 text-sm font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors",
|
|
11
|
+
form: { class: "inline" },
|
|
12
|
+
data: { confirm: "Are you sure you want to clear all captured queries?" } %>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- Query Statistics -->
|
|
17
|
+
<div class="bg-white rounded-lg shadow">
|
|
18
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
19
|
+
<h2 class="text-lg font-medium text-gray-900">Query Statistics</h2>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="p-6">
|
|
22
|
+
<div class="grid grid-cols-1 md:grid-cols-5 gap-6">
|
|
23
|
+
<div class="text-center">
|
|
24
|
+
<div class="text-2xl font-bold text-blue-600"><%= @queries.length %></div>
|
|
25
|
+
<div class="text-sm text-gray-500">Total Queries</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="text-center">
|
|
28
|
+
<div class="text-2xl font-bold text-green-600">
|
|
29
|
+
<%= @queries.any? ? format("%.1f", @queries.map(&:duration_ms).sum / @queries.length) : "0.0" %>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="text-sm text-gray-500">Avg Duration (ms)</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="text-center">
|
|
34
|
+
<div class="text-2xl font-bold text-orange-600">
|
|
35
|
+
<%= @queries.any? ? format("%.1f", @queries.map(&:duration_ms).max || 0) : "0.0" %>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="text-sm text-gray-500">Slowest (ms)</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="text-center">
|
|
40
|
+
<div class="text-2xl font-bold text-purple-600"><%= @queries.select { |q| q.sql.downcase.start_with?('select') }.length %></div>
|
|
41
|
+
<div class="text-sm text-gray-500">SELECT Queries</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="text-center">
|
|
44
|
+
<div class="text-2xl font-bold <%= @n_plus_ones.any? ? 'text-red-600' : 'text-green-600' %>"><%= @n_plus_ones.length %></div>
|
|
45
|
+
<div class="text-sm text-gray-500">N+1 Detected</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<!-- N+1 Query Warnings -->
|
|
52
|
+
<% if @n_plus_ones.any? %>
|
|
53
|
+
<div class="bg-red-50 border border-red-200 rounded-lg shadow overflow-hidden">
|
|
54
|
+
<div class="px-6 py-4 border-b border-red-200 bg-red-100">
|
|
55
|
+
<div class="flex items-center">
|
|
56
|
+
<span class="text-2xl mr-3">🔁</span>
|
|
57
|
+
<div>
|
|
58
|
+
<h2 class="text-lg font-semibold text-red-900">N+1 Query Detection</h2>
|
|
59
|
+
<p class="text-sm text-red-700">
|
|
60
|
+
Found <%= pluralize(@n_plus_ones.length, 'potential N+1 pattern') %>.
|
|
61
|
+
These are repeated identical queries that likely belong in a single query with eager loading.
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="divide-y divide-red-200">
|
|
67
|
+
<% @n_plus_ones.each do |n1| %>
|
|
68
|
+
<div class="px-6 py-4">
|
|
69
|
+
<div class="flex items-start justify-between">
|
|
70
|
+
<div class="flex-1 min-w-0">
|
|
71
|
+
<div class="flex items-center gap-2 mb-1">
|
|
72
|
+
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold bg-red-600 text-white">
|
|
73
|
+
×<%= n1[:count] %>
|
|
74
|
+
</span>
|
|
75
|
+
<span class="text-sm font-semibold text-red-900"><%= n1[:name] || "Query" %></span>
|
|
76
|
+
<span class="text-xs text-red-600">on <code class="font-mono"><%= n1[:table] %></code></span>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="bg-white rounded p-2 mt-2 border border-red-200">
|
|
79
|
+
<code class="text-xs font-mono text-gray-700 break-all"><%= truncate(n1[:sample].sql.gsub(/\s+/, " "), length: 200) %></code>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="mt-2 flex items-center gap-4 text-xs text-red-700">
|
|
82
|
+
<span>Total time: <strong><%= format("%.1f", n1[:total_duration_ms]) %>ms</strong></span>
|
|
83
|
+
<span>Avg: <strong><%= format("%.1f", n1[:total_duration_ms] / n1[:count]) %>ms</strong> per query</span>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="mt-2 bg-yellow-50 border border-yellow-200 rounded p-2">
|
|
86
|
+
<p class="text-xs text-yellow-800">
|
|
87
|
+
<strong>💡 Fix:</strong> Add <code class="font-mono bg-yellow-100 px-1 rounded">includes(:<%= n1[:table].singularize rescue n1[:table] %>)</code>
|
|
88
|
+
or <code class="font-mono bg-yellow-100 px-1 rounded">eager_load(:<%= n1[:table].singularize rescue n1[:table] %>)</code>
|
|
89
|
+
to the parent query to load all associated records in a single query.
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="ml-4 flex-shrink-0">
|
|
94
|
+
<%= link_to "View", query_path(n1[:sample].id),
|
|
95
|
+
class: "inline-flex items-center px-2 py-1 border border-red-300 rounded text-xs text-red-700 bg-white hover:bg-red-50 transition-colors" %>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<% end %>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<% end %>
|
|
103
|
+
|
|
104
|
+
<!-- Queries Table -->
|
|
105
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
106
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
107
|
+
<h2 class="text-lg font-medium text-gray-900">Recent Queries</h2>
|
|
108
|
+
<p class="mt-1 text-sm text-gray-500">Grouped by action/request - click on any query to view details or generate execution plans</p>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<% if @query_groups.any? %>
|
|
112
|
+
<div class="w-full">
|
|
113
|
+
<table class="w-full divide-y divide-gray-200" style="table-layout: fixed;">
|
|
114
|
+
<colgroup>
|
|
115
|
+
<col style="width: 70px;">
|
|
116
|
+
<col style="width: 75px;">
|
|
117
|
+
<col style="width: 85px;">
|
|
118
|
+
<col style="width: 43%;">
|
|
119
|
+
<col style="width: 90px;">
|
|
120
|
+
<col style="width: 90px;">
|
|
121
|
+
</colgroup>
|
|
122
|
+
<thead class="bg-gray-50">
|
|
123
|
+
<tr>
|
|
124
|
+
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
|
|
125
|
+
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
|
|
126
|
+
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
|
127
|
+
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">SQL</th>
|
|
128
|
+
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
|
129
|
+
<th class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
130
|
+
</tr>
|
|
131
|
+
</thead>
|
|
132
|
+
<tbody class="bg-white">
|
|
133
|
+
<% @query_groups.each_with_index do |group, group_index| %>
|
|
134
|
+
<!-- Group Header -->
|
|
135
|
+
<tr class="bg-blue-50 border-t-2 border-blue-200">
|
|
136
|
+
<td colspan="6" class="px-6 py-3">
|
|
137
|
+
<div class="flex items-center justify-between">
|
|
138
|
+
<div class="flex items-center space-x-3">
|
|
139
|
+
<span class="text-lg"><%= group_icon(group[:request_type]) %></span>
|
|
140
|
+
<div>
|
|
141
|
+
<h3 class="text-sm font-semibold text-blue-900"><%= group[:action] %></h3>
|
|
142
|
+
<p class="text-xs text-blue-600">
|
|
143
|
+
<%= format_group_time_range(group) %> •
|
|
144
|
+
<%= pluralize(group[:queries].length, 'query') %> •
|
|
145
|
+
<%= format("%.1f", group[:queries].sum(&:duration_ms)) %>ms total
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="flex items-center space-x-2">
|
|
150
|
+
<% if group[:queries].any? { |q| q.duration_ms > 100 } %>
|
|
151
|
+
<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
152
|
+
⚠️ Slow Queries
|
|
153
|
+
</span>
|
|
154
|
+
<% end %>
|
|
155
|
+
<% if group[:queries].length > 10 %>
|
|
156
|
+
<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-orange-100 text-orange-800">
|
|
157
|
+
📊 High Volume
|
|
158
|
+
</span>
|
|
159
|
+
<% end %>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</td>
|
|
163
|
+
</tr>
|
|
164
|
+
|
|
165
|
+
<!-- Group Queries -->
|
|
166
|
+
<% group[:queries].each_with_index do |q, index| %>
|
|
167
|
+
<tr class="<%= 'bg-red-50' if q.duration_ms > 1000 %> <%= 'bg-yellow-50' if q.duration_ms > 100 && q.duration_ms <= 1000 %> hover:bg-gray-50 transition-colors <%= 'border-b-2 border-gray-300' if index == group[:queries].length - 1 && group_index < @query_groups.length - 1 %>">
|
|
168
|
+
<td class="px-2 py-2 whitespace-nowrap text-sm text-gray-500">
|
|
169
|
+
<% ts = q.timestamp %>
|
|
170
|
+
<%= if ts
|
|
171
|
+
(ts.is_a?(Time) ? ts : Time.at(ts.to_f)).strftime("%H:%M:%S")
|
|
172
|
+
end %>
|
|
173
|
+
</td>
|
|
174
|
+
<td class="px-2 py-2 whitespace-nowrap">
|
|
175
|
+
<span class="text-sm font-medium <%= 'text-red-600' if q.duration_ms > 1000 %> <%= 'text-yellow-600' if q.duration_ms > 100 && q.duration_ms <= 1000 %> <%= 'text-gray-900' if q.duration_ms <= 100 %>">
|
|
176
|
+
<%= format("%.1f", q.duration_ms) %>ms
|
|
177
|
+
</span>
|
|
178
|
+
</td>
|
|
179
|
+
<td class="px-2 py-2 text-sm text-gray-900 truncate">
|
|
180
|
+
<span class="font-medium"><%= q.name || 'Anonymous' %></span>
|
|
181
|
+
</td>
|
|
182
|
+
<td class="px-2 py-2">
|
|
183
|
+
<div class="flex items-center gap-1">
|
|
184
|
+
<span class="sql-hover-trigger flex-shrink-0 inline-flex items-center justify-center w-4 h-4 rounded-full bg-gray-200 text-gray-500 text-xs cursor-pointer hover:bg-blue-200 hover:text-blue-700" title="View full query">
|
|
185
|
+
i
|
|
186
|
+
<span class="sql-tooltip hidden absolute z-50 mt-1 max-w-3xl bg-gray-900 text-gray-100 text-xs font-mono p-3 rounded-lg shadow-xl whitespace-pre-wrap break-all">
|
|
187
|
+
<%= q.sql %>
|
|
188
|
+
</span>
|
|
189
|
+
</span>
|
|
190
|
+
<span class="text-sm font-mono text-gray-700 truncate">
|
|
191
|
+
<%= truncate(q.sql.gsub(/\s+/, " "), length: 100) %>
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
</td>
|
|
195
|
+
<td class="px-2 py-2 text-xs">
|
|
196
|
+
<div class="flex flex-wrap gap-1">
|
|
197
|
+
<%= render_query_type(q) %>
|
|
198
|
+
</div>
|
|
199
|
+
</td>
|
|
200
|
+
<td class="px-2 py-2 whitespace-nowrap text-xs font-medium">
|
|
201
|
+
<div class="flex space-x-1">
|
|
202
|
+
<%= link_to "View", query_path(q.id),
|
|
203
|
+
class: "inline-flex items-center px-2 py-1 border border-blue-300 rounded text-blue-700 bg-white hover:bg-blue-50 transition-colors" %>
|
|
204
|
+
<%= link_to "Explain", explain_query_path(q.id),
|
|
205
|
+
class: "inline-flex items-center px-2 py-1 border border-green-300 rounded text-green-700 bg-white hover:bg-green-50 transition-colors" %>
|
|
206
|
+
</div>
|
|
207
|
+
</td>
|
|
208
|
+
</tr>
|
|
209
|
+
<% end %>
|
|
210
|
+
<% end %>
|
|
211
|
+
</tbody>
|
|
212
|
+
</table>
|
|
213
|
+
</div>
|
|
214
|
+
<% else %>
|
|
215
|
+
<div class="px-6 py-12 text-center">
|
|
216
|
+
<div class="text-gray-400 mb-4">
|
|
217
|
+
<svg class="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
218
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
219
|
+
</svg>
|
|
220
|
+
</div>
|
|
221
|
+
<h3 class="text-lg font-medium text-gray-900 mb-2">No queries captured yet</h3>
|
|
222
|
+
<p class="text-gray-500 mb-4">SQL queries will appear here as they are executed by your Rails application.</p>
|
|
223
|
+
<p class="text-sm text-gray-400">Make sure your Rails app is running and executing database queries.</p>
|
|
224
|
+
</div>
|
|
225
|
+
<% end %>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<style>
|
|
230
|
+
.sql-hover-trigger {
|
|
231
|
+
position: relative;
|
|
232
|
+
}
|
|
233
|
+
.sql-tooltip {
|
|
234
|
+
position: fixed;
|
|
235
|
+
pointer-events: none;
|
|
236
|
+
}
|
|
237
|
+
</style>
|
|
238
|
+
|
|
239
|
+
<script>
|
|
240
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
241
|
+
document.querySelectorAll('.sql-hover-trigger').forEach(function(el) {
|
|
242
|
+
var tooltip = el.querySelector('.sql-tooltip');
|
|
243
|
+
if (!tooltip) return;
|
|
244
|
+
|
|
245
|
+
el.addEventListener('mouseenter', function(e) {
|
|
246
|
+
var rect = el.getBoundingClientRect();
|
|
247
|
+
tooltip.style.top = (rect.bottom + 4) + 'px';
|
|
248
|
+
tooltip.style.left = rect.left + 'px';
|
|
249
|
+
tooltip.style.maxWidth = Math.min(800, window.innerWidth - rect.left - 20) + 'px';
|
|
250
|
+
tooltip.classList.remove('hidden');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
el.addEventListener('mouseleave', function() {
|
|
254
|
+
tooltip.classList.add('hidden');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
</script>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<div class="space-y-6">
|
|
2
|
+
<!-- Header -->
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<h1 class="text-3xl font-bold text-gray-900">Query Details</h1>
|
|
6
|
+
<p class="mt-2 text-gray-600">Detailed information about the captured SQL query</p>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="flex space-x-3">
|
|
9
|
+
<%= link_to "← Back to List", queries_path,
|
|
10
|
+
class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors" %>
|
|
11
|
+
<%= link_to "Explain Plan", explain_query_path(@query.id),
|
|
12
|
+
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors" %>
|
|
13
|
+
<%= link_to "Explain Analyze", explain_query_path(@query.id, analyze: true),
|
|
14
|
+
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors" %>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Query Metadata -->
|
|
19
|
+
<div class="bg-white rounded-lg shadow">
|
|
20
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
21
|
+
<h2 class="text-lg font-medium text-gray-900">Query Metadata</h2>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="p-6">
|
|
24
|
+
<dl class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
25
|
+
<div>
|
|
26
|
+
<dt class="text-sm font-medium text-gray-500">Query Name</dt>
|
|
27
|
+
<dd class="mt-1 text-sm text-gray-900"><%= @query.name || 'Anonymous Query' %></dd>
|
|
28
|
+
</div>
|
|
29
|
+
<div>
|
|
30
|
+
<dt class="text-sm font-medium text-gray-500">Duration</dt>
|
|
31
|
+
<dd class="mt-1">
|
|
32
|
+
<span class="text-sm font-medium <%= 'text-red-600' if @query.duration_ms > 1000 %> <%= 'text-yellow-600' if @query.duration_ms > 100 && @query.duration_ms <= 1000 %> <%= 'text-green-600' if @query.duration_ms <= 100 %>">
|
|
33
|
+
<%= format("%.1f", @query.duration_ms) %> ms
|
|
34
|
+
</span>
|
|
35
|
+
</dd>
|
|
36
|
+
</div>
|
|
37
|
+
<div>
|
|
38
|
+
<dt class="text-sm font-medium text-gray-500">Timestamp</dt>
|
|
39
|
+
<dd class="mt-1 text-sm text-gray-900">
|
|
40
|
+
<% ts = @query.timestamp %>
|
|
41
|
+
<%= if ts
|
|
42
|
+
(ts.is_a?(Time) ? ts : Time.at(ts.to_f)).strftime("%Y-%m-%d %H:%M:%S")
|
|
43
|
+
end %>
|
|
44
|
+
</dd>
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<dt class="text-sm font-medium text-gray-500">Query Type</dt>
|
|
48
|
+
<dd class="mt-1"><%= render_query_type(@query) %></dd>
|
|
49
|
+
</div>
|
|
50
|
+
</dl>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<!-- SQL Query -->
|
|
55
|
+
<div class="bg-white rounded-lg shadow">
|
|
56
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
57
|
+
<h2 class="text-lg font-medium text-gray-900">SQL Statement</h2>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="p-6">
|
|
60
|
+
<div class="bg-gray-900 rounded-md p-4 overflow-x-auto">
|
|
61
|
+
<pre class="text-green-400 text-sm font-mono whitespace-pre-wrap"><%= @query.sql %></pre>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Query Binds -->
|
|
67
|
+
<% if @query.binds && !@query.binds.empty? %>
|
|
68
|
+
<div class="bg-white rounded-lg shadow">
|
|
69
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
70
|
+
<h2 class="text-lg font-medium text-gray-900">Parameter Bindings</h2>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="p-6">
|
|
73
|
+
<div class="bg-gray-50 rounded-md p-4">
|
|
74
|
+
<pre class="text-sm font-mono text-gray-800"><%= @query.binds.inspect %></pre>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<% end %>
|
|
79
|
+
|
|
80
|
+
<!-- Performance Tips -->
|
|
81
|
+
<div class="bg-blue-50 border border-blue-200 rounded-lg">
|
|
82
|
+
<div class="p-6">
|
|
83
|
+
<div class="flex">
|
|
84
|
+
<div class="flex-shrink-0">
|
|
85
|
+
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
|
|
86
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
|
87
|
+
</svg>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="ml-3">
|
|
90
|
+
<h3 class="text-sm font-medium text-blue-800">Performance Analysis</h3>
|
|
91
|
+
<div class="mt-2 text-sm text-blue-700">
|
|
92
|
+
<p>Click "Explain Plan" to see the query execution strategy, or "Explain Analyze" for live performance metrics with actual timing data.</p>
|
|
93
|
+
<% if @query.duration_ms > 1000 %>
|
|
94
|
+
<p class="mt-2 font-medium text-red-700">⚠️ This query took over 1 second to execute - consider optimization.</p>
|
|
95
|
+
<% elsif @query.duration_ms > 100 %>
|
|
96
|
+
<p class="mt-2 font-medium text-yellow-700">💡 This query took over 100ms - might benefit from index optimization.</p>
|
|
97
|
+
<% end %>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|