dbviewer 0.3.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +250 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/dbviewer/application.css +21 -0
- data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
- data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
- data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
- data/app/controllers/dbviewer/application_controller.rb +21 -0
- data/app/controllers/dbviewer/databases_controller.rb +0 -0
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
- data/app/controllers/dbviewer/home_controller.rb +10 -0
- data/app/controllers/dbviewer/logs_controller.rb +39 -0
- data/app/controllers/dbviewer/tables_controller.rb +73 -0
- data/app/helpers/dbviewer/application_helper.rb +118 -0
- data/app/jobs/dbviewer/application_job.rb +4 -0
- data/app/mailers/dbviewer/application_mailer.rb +6 -0
- data/app/models/dbviewer/application_record.rb +5 -0
- data/app/services/dbviewer/file_storage.rb +0 -0
- data/app/services/dbviewer/in_memory_storage.rb +0 -0
- data/app/services/dbviewer/query_analyzer.rb +0 -0
- data/app/services/dbviewer/query_collection.rb +0 -0
- data/app/services/dbviewer/query_logger.rb +0 -0
- data/app/services/dbviewer/query_parser.rb +82 -0
- data/app/services/dbviewer/query_storage.rb +0 -0
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
- data/app/views/dbviewer/home/index.html.erb +237 -0
- data/app/views/dbviewer/logs/index.html.erb +614 -0
- data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
- data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
- data/app/views/dbviewer/tables/index.html.erb +128 -0
- data/app/views/dbviewer/tables/query.html.erb +600 -0
- data/app/views/dbviewer/tables/show.html.erb +271 -0
- data/app/views/layouts/dbviewer/application.html.erb +728 -0
- data/config/routes.rb +22 -0
- data/lib/dbviewer/configuration.rb +79 -0
- data/lib/dbviewer/database_manager.rb +450 -0
- data/lib/dbviewer/engine.rb +20 -0
- data/lib/dbviewer/initializer.rb +23 -0
- data/lib/dbviewer/logger.rb +102 -0
- data/lib/dbviewer/query_analyzer.rb +109 -0
- data/lib/dbviewer/query_collection.rb +41 -0
- data/lib/dbviewer/query_parser.rb +82 -0
- data/lib/dbviewer/sql_validator.rb +194 -0
- data/lib/dbviewer/storage/base.rb +31 -0
- data/lib/dbviewer/storage/file_storage.rb +96 -0
- data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
- data/lib/dbviewer/version.rb +3 -0
- data/lib/dbviewer.rb +65 -0
- data/lib/tasks/dbviewer_tasks.rake +4 -0
- metadata +126 -0
@@ -0,0 +1,614 @@
|
|
1
|
+
<% content_for :title, "SQL Query Logs" %>
|
2
|
+
|
3
|
+
<% content_for :sidebar do %>
|
4
|
+
<%= render 'dbviewer/shared/sidebar' %>
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<div class="container-fluid">
|
8
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
9
|
+
<h1>
|
10
|
+
<i class="bi bi-journal-code me-2"></i>SQL Query Logs
|
11
|
+
</h1>
|
12
|
+
<div>
|
13
|
+
<% if @filtered_stats %>
|
14
|
+
<%= link_to logs_path, class: "btn btn-outline-secondary me-2" do %>
|
15
|
+
<i class="bi bi-x-lg me-1"></i> Clear Filters
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
<%= button_to destroy_all_logs_path,
|
19
|
+
class: "btn btn-outline-danger",
|
20
|
+
method: :delete do %>
|
21
|
+
<i class="bi bi-trash3"></i> Clear Logs
|
22
|
+
<% end %>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
|
26
|
+
<% if @filtered_stats %>
|
27
|
+
<div class="alert alert-info mb-4 d-flex justify-content-between align-items-center">
|
28
|
+
<div>
|
29
|
+
<i class="bi bi-funnel-fill me-2"></i>
|
30
|
+
<strong>Filtered view:</strong>
|
31
|
+
<% filter_parts = [] %>
|
32
|
+
<% filter_parts << "Request ID: #{@request_id}" if @request_id.present? %>
|
33
|
+
<% filter_parts << "Table: #{@table_filter}" if @table_filter.present? %>
|
34
|
+
<% filter_parts << "Min Duration: #{@min_duration} ms" if @min_duration.present? %>
|
35
|
+
<%= filter_parts.join(", ") %>
|
36
|
+
</div>
|
37
|
+
<span class="badge bg-primary"><%= @stats[:total_count] %> matching queries</span>
|
38
|
+
</div>
|
39
|
+
<% end %>
|
40
|
+
|
41
|
+
<!-- Stats Cards -->
|
42
|
+
<div class="row mb-4">
|
43
|
+
<div class="col-md-3">
|
44
|
+
<div class="card h-100 <%= 'border-info' if @filtered_stats %>">
|
45
|
+
<div class="card-body">
|
46
|
+
<div class="d-flex justify-content-between">
|
47
|
+
<h5 class="card-title">Queries</h5>
|
48
|
+
<% if @filtered_stats %>
|
49
|
+
<span class="badge bg-info text-dark">Filtered</span>
|
50
|
+
<% end %>
|
51
|
+
</div>
|
52
|
+
<h2><%= @stats[:total_count] %></h2>
|
53
|
+
<div class="stats-detail small mt-2">
|
54
|
+
<div>Requests: <%= @stats[:request_count] || 0 %></div>
|
55
|
+
<div>Avg per request: <%= number_with_precision(@stats[:avg_queries_per_request] || 0, precision: 1) %></div>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
<div class="col-md-3">
|
61
|
+
<div class="card h-100 <%= 'border-info' if @filtered_stats %>">
|
62
|
+
<div class="card-body">
|
63
|
+
<div class="d-flex justify-content-between">
|
64
|
+
<h5 class="card-title">Total Duration</h5>
|
65
|
+
<% if @filtered_stats %>
|
66
|
+
<span class="badge bg-info text-dark">Filtered</span>
|
67
|
+
<% end %>
|
68
|
+
</div>
|
69
|
+
<h2><%= number_with_precision(@stats[:total_duration_ms], precision: 2) %> ms</h2>
|
70
|
+
<div class="stats-detail small mt-2">
|
71
|
+
<div>Max per request: <%= @stats[:max_queries_per_request] || 0 %> queries</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
<div class="col-md-3">
|
77
|
+
<div class="card h-100 <%= 'border-info' if @filtered_stats %>">
|
78
|
+
<div class="card-body">
|
79
|
+
<div class="d-flex justify-content-between">
|
80
|
+
<h5 class="card-title">Average Duration</h5>
|
81
|
+
<% if @filtered_stats %>
|
82
|
+
<span class="badge bg-info text-dark">Filtered</span>
|
83
|
+
<% end %>
|
84
|
+
</div>
|
85
|
+
<h2><%= number_with_precision(@stats[:avg_duration_ms], precision: 2) %> ms</h2>
|
86
|
+
<div class="stats-detail small mt-2">
|
87
|
+
<div>Per query</div>
|
88
|
+
</div>
|
89
|
+
</div>
|
90
|
+
</div>
|
91
|
+
</div>
|
92
|
+
<div class="col-md-3">
|
93
|
+
<div class="card h-100 <%= 'border-info' if @filtered_stats %>">
|
94
|
+
<div class="card-body">
|
95
|
+
<div class="d-flex justify-content-between">
|
96
|
+
<h5 class="card-title">Max Duration</h5>
|
97
|
+
<% if @filtered_stats %>
|
98
|
+
<span class="badge bg-info text-dark">Filtered</span>
|
99
|
+
<% end %>
|
100
|
+
</div>
|
101
|
+
<h2><%= number_with_precision(@stats[:max_duration_ms], precision: 2) %> ms</h2>
|
102
|
+
<div class="text-muted small mt-2">
|
103
|
+
<div>Slowest query</div>
|
104
|
+
</div>
|
105
|
+
</div>
|
106
|
+
</div>
|
107
|
+
</div>
|
108
|
+
</div>
|
109
|
+
|
110
|
+
<!-- N+1 Query Warnings -->
|
111
|
+
<% if @stats[:potential_n_plus_1].present? %>
|
112
|
+
<div class="card mb-4 <%= 'border-info' if @filtered_stats %>">
|
113
|
+
<div class="card-header <%= @filtered_stats ? 'bg-info-subtle' : 'bg-warning-subtle' %> cursor-pointer"
|
114
|
+
data-bs-toggle="collapse" data-bs-target="#n1QueriesCollapse" aria-expanded="false" aria-controls="n1QueriesCollapse">
|
115
|
+
<div class="d-flex justify-content-between align-items-center">
|
116
|
+
<h5 class="card-title mb-0 <%= @filtered_stats ? 'text-info' : 'text-warning' %>">
|
117
|
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
118
|
+
Potential N+1 Query Issues
|
119
|
+
<% if @filtered_stats %>
|
120
|
+
<span class="badge bg-info text-dark ms-2">Filtered</span>
|
121
|
+
<% end %>
|
122
|
+
</h5>
|
123
|
+
<div>
|
124
|
+
<span class="badge <%= @filtered_stats ? 'bg-info' : 'bg-warning' %> me-2">
|
125
|
+
<%= @stats[:potential_n_plus_1].size %> patterns detected
|
126
|
+
</span>
|
127
|
+
<i class="bi bi-chevron-down n1-collapse-icon"></i>
|
128
|
+
</div>
|
129
|
+
</div>
|
130
|
+
</div>
|
131
|
+
<div class="collapse" id="n1QueriesCollapse">
|
132
|
+
<div class="card-body">
|
133
|
+
<p class="text-muted mb-3">
|
134
|
+
These query patterns might indicate N+1 query problems. Consider using eager loading, joins, or batch loading to optimize them.
|
135
|
+
</p>
|
136
|
+
|
137
|
+
<div class="list-group">
|
138
|
+
<% @stats[:potential_n_plus_1].each do |issue| %>
|
139
|
+
<div class="list-group-item">
|
140
|
+
<div class="d-flex justify-content-between align-items-start mb-2">
|
141
|
+
<h6 class="mb-0">
|
142
|
+
<%= issue[:table] ? "Table: <strong>#{issue[:table]}</strong>".html_safe : "Multiple tables" %>
|
143
|
+
</h6>
|
144
|
+
<div>
|
145
|
+
<span class="badge bg-danger me-1" title="Number of similar queries">
|
146
|
+
<%= issue[:count] %> similar queries
|
147
|
+
</span>
|
148
|
+
<span class="badge bg-warning" title="Total time spent">
|
149
|
+
<%= number_with_precision(issue[:total_duration_ms], precision: 1) %> ms total
|
150
|
+
</span>
|
151
|
+
</div>
|
152
|
+
</div>
|
153
|
+
|
154
|
+
<a href="<%= logs_path(request_id: issue[:request_id]) %>"
|
155
|
+
class="small d-block mb-1 text-muted">
|
156
|
+
<i class="bi bi-link-45deg"></i>
|
157
|
+
Request: <%= issue[:request_id] %>
|
158
|
+
</a>
|
159
|
+
|
160
|
+
<div class="p-2 sql-code-block rounded small">
|
161
|
+
<code class="d-block pattern-code" style="white-space: pre-wrap;"><%= issue[:pattern] %></code>
|
162
|
+
</div>
|
163
|
+
</div>
|
164
|
+
<% end %>
|
165
|
+
</div>
|
166
|
+
</div>
|
167
|
+
</div>
|
168
|
+
</div>
|
169
|
+
<% end %>
|
170
|
+
|
171
|
+
<!-- Top 5 Slowest Queries -->
|
172
|
+
<% if @stats[:slowest_queries].present? %>
|
173
|
+
<div class="card mb-4 <%= 'border-info' if @filtered_stats %>">
|
174
|
+
<div class="card-header <%= @filtered_stats ? 'bg-info-subtle' : 'bg-danger-subtle' %> cursor-pointer"
|
175
|
+
data-bs-toggle="collapse" data-bs-target="#slowestQueriesCollapse" aria-expanded="false" aria-controls="slowestQueriesCollapse">
|
176
|
+
<div class="d-flex justify-content-between align-items-center">
|
177
|
+
<h5 class="card-title mb-0 <%= @filtered_stats ? 'text-info' : 'text-danger' %>">
|
178
|
+
<i class="bi bi-hourglass-split me-2"></i>
|
179
|
+
Top 5 Slowest Queries
|
180
|
+
<% if @filtered_stats %>
|
181
|
+
<span class="badge bg-info text-dark ms-2">Filtered</span>
|
182
|
+
<% end %>
|
183
|
+
</h5>
|
184
|
+
<div>
|
185
|
+
<span class="badge bg-danger me-2">
|
186
|
+
<%= @stats[:slowest_queries].size %> queries
|
187
|
+
</span>
|
188
|
+
<i class="bi bi-chevron-down slowest-collapse-icon"></i>
|
189
|
+
</div>
|
190
|
+
</div>
|
191
|
+
</div>
|
192
|
+
<div class="collapse" id="slowestQueriesCollapse">
|
193
|
+
<div class="card-body p-0">
|
194
|
+
<div class="list-group list-group-flush">
|
195
|
+
<% @stats[:slowest_queries].each_with_index do |query, index| %>
|
196
|
+
<div class="list-group-item">
|
197
|
+
<div class="d-flex justify-content-between align-items-start mb-2">
|
198
|
+
<span class="badge bg-danger me-2 fs-6">#<%= index + 1 %></span>
|
199
|
+
<h6 class="mb-0 flex-grow-1">
|
200
|
+
<%= query[:name] %>
|
201
|
+
</h6>
|
202
|
+
<div>
|
203
|
+
<span class="badge bg-danger" title="Query duration">
|
204
|
+
<%= number_with_precision(query[:duration_ms], precision: 2) %> ms
|
205
|
+
</span>
|
206
|
+
</div>
|
207
|
+
</div>
|
208
|
+
|
209
|
+
<div class="d-flex justify-content-between small text-muted mb-2">
|
210
|
+
<span>
|
211
|
+
<i class="bi bi-clock me-1"></i>
|
212
|
+
<%= query[:timestamp].strftime("%H:%M:%S.%L") %>
|
213
|
+
</span>
|
214
|
+
<a href="<%= logs_path(request_id: query[:request_id]) %>" class="text-muted">
|
215
|
+
<i class="bi bi-link-45deg"></i>
|
216
|
+
Request: <%= query[:request_id] %>
|
217
|
+
</a>
|
218
|
+
</div>
|
219
|
+
|
220
|
+
<div class="p-2 sql-code-block rounded small">
|
221
|
+
<pre class="mb-0 sql-query"><code><%= query[:sql] %></code></pre>
|
222
|
+
</div>
|
223
|
+
</div>
|
224
|
+
<% end %>
|
225
|
+
</div>
|
226
|
+
</div>
|
227
|
+
</div>
|
228
|
+
</div>
|
229
|
+
<% end %>
|
230
|
+
|
231
|
+
<!-- Table Access Chart -->
|
232
|
+
<% if @stats[:tables_queried].present? %>
|
233
|
+
<div class="row mb-4">
|
234
|
+
<div class="col-md-12">
|
235
|
+
<div class="card <%= 'border-info' if @filtered_stats %>">
|
236
|
+
<div class="card-header <%= @filtered_stats ? 'bg-info-subtle' : '' %>">
|
237
|
+
<div class="d-flex justify-content-between align-items-center">
|
238
|
+
<h5 class="card-title mb-0">Table Access Frequency</h5>
|
239
|
+
<% if @filtered_stats %>
|
240
|
+
<span class="badge bg-info text-dark">Filtered</span>
|
241
|
+
<% end %>
|
242
|
+
</div>
|
243
|
+
</div>
|
244
|
+
<div class="card-body">
|
245
|
+
<canvas id="tablesChart" width="400" height="150"></canvas>
|
246
|
+
</div>
|
247
|
+
</div>
|
248
|
+
</div>
|
249
|
+
</div>
|
250
|
+
<% end %>
|
251
|
+
|
252
|
+
<!-- Filters -->
|
253
|
+
<div class="card mb-4">
|
254
|
+
<div class="card-header">
|
255
|
+
<h5 class="card-title mb-0">Filter Queries</h5>
|
256
|
+
</div>
|
257
|
+
<div class="card-body">
|
258
|
+
<%= form_with url: logs_path, method: :get, class: "row g-3" do |f| %>
|
259
|
+
<div class="col-md-3">
|
260
|
+
<label for="table_filter" class="form-label">Table Name</label>
|
261
|
+
<input type="text" class="form-control" id="table_filter" name="table_filter"
|
262
|
+
placeholder="Filter by table name" value="<%= @table_filter %>">
|
263
|
+
</div>
|
264
|
+
<div class="col-md-3">
|
265
|
+
<label for="request_id" class="form-label">Request ID</label>
|
266
|
+
<input type="text" class="form-control" id="request_id" name="request_id"
|
267
|
+
placeholder="Filter by request ID" value="<%= @request_id %>">
|
268
|
+
</div>
|
269
|
+
<div class="col-md-2">
|
270
|
+
<label for="min_duration" class="form-label">Min Duration (ms)</label>
|
271
|
+
<input type="number" class="form-control" id="min_duration" name="min_duration"
|
272
|
+
placeholder="e.g., 100" min="0" step="0.1" value="<%= @min_duration %>">
|
273
|
+
</div>
|
274
|
+
<div class="col-md-2">
|
275
|
+
<label for="limit" class="form-label">Result Limit</label>
|
276
|
+
<input type="number" class="form-control" id="limit" name="limit"
|
277
|
+
placeholder="Max results" min="1" max="1000" value="<%= @limit %>">
|
278
|
+
</div>
|
279
|
+
<div class="col-md-2 d-flex align-items-end">
|
280
|
+
<div class="d-flex gap-2 w-100">
|
281
|
+
<button type="submit" class="btn btn-primary flex-grow-1">
|
282
|
+
<i class="bi bi-funnel"></i> Apply
|
283
|
+
</button>
|
284
|
+
<% if @filtered_stats %>
|
285
|
+
<a href="<%= logs_path %>" class="btn btn-outline-secondary">
|
286
|
+
<i class="bi bi-x-lg"></i>
|
287
|
+
</a>
|
288
|
+
<% end %>
|
289
|
+
</div>
|
290
|
+
</div>
|
291
|
+
<% end %>
|
292
|
+
</div>
|
293
|
+
</div>
|
294
|
+
|
295
|
+
<!-- Query Logs Table -->
|
296
|
+
<div class="card">
|
297
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
298
|
+
<h5 class="card-title mb-0">Query Logs</h5>
|
299
|
+
<div>
|
300
|
+
<span class="badge bg-secondary me-2"><%= @queries.size %> queries</span>
|
301
|
+
<span class="badge bg-info"><%= @queries.map {|q| q[:request_id] }.uniq.size %> requests</span>
|
302
|
+
</div>
|
303
|
+
</div>
|
304
|
+
<div class="card-body p-0">
|
305
|
+
<div class="table-responsive">
|
306
|
+
<table class="table table-hover table-striped mb-0">
|
307
|
+
<thead class="dbviewer-table-header">
|
308
|
+
<tr>
|
309
|
+
<th style="width: 3%;">#</th>
|
310
|
+
<th style="width: 13%;">Timestamp</th>
|
311
|
+
<th style="width: 8%;">Duration</th>
|
312
|
+
<th style="width: 14%;">Request ID</th>
|
313
|
+
<th style="width: 10%;">Name</th>
|
314
|
+
<th style="width: 52%;">SQL Query</th>
|
315
|
+
</tr>
|
316
|
+
</thead>
|
317
|
+
<tbody>
|
318
|
+
<% if @queries.present? %>
|
319
|
+
<%
|
320
|
+
current_request_id = nil
|
321
|
+
@queries.sort_by { |q| q[:request_id] }.each_with_index do |query, index|
|
322
|
+
new_request = current_request_id != query[:request_id]
|
323
|
+
current_request_id = query[:request_id]
|
324
|
+
|
325
|
+
# Count queries in this request group
|
326
|
+
request_query_count = @queries.count { |q| q[:request_id] == current_request_id }
|
327
|
+
total_request_time = @queries.select { |q| q[:request_id] == current_request_id }
|
328
|
+
.sum { |q| q[:duration_ms] }
|
329
|
+
%>
|
330
|
+
<% if new_request %>
|
331
|
+
<tr class="request-group-header request-header-bg">
|
332
|
+
<td colspan="6" class="py-1">
|
333
|
+
<div class="d-flex justify-content-between align-items-center">
|
334
|
+
<span class="fw-bold">
|
335
|
+
<i class="bi bi-shuffle me-2"></i>
|
336
|
+
Request: <%= current_request_id %>
|
337
|
+
</span>
|
338
|
+
<div>
|
339
|
+
<span class="badge bg-primary me-2" title="Number of queries in this request">
|
340
|
+
<%= request_query_count %> queries
|
341
|
+
</span>
|
342
|
+
<span class="badge bg-secondary" title="Total time for this request">
|
343
|
+
<%= number_with_precision(total_request_time, precision: 2) %> ms
|
344
|
+
</span>
|
345
|
+
</div>
|
346
|
+
</div>
|
347
|
+
</td>
|
348
|
+
</tr>
|
349
|
+
<% end %>
|
350
|
+
<tr>
|
351
|
+
<td><%= index + 1 %></td>
|
352
|
+
<td><%= query[:timestamp].strftime("%H:%M:%S.%L") %></td>
|
353
|
+
<td>
|
354
|
+
<% duration_class = case
|
355
|
+
when query[:duration_ms] > 500 then "text-danger fw-bold"
|
356
|
+
when query[:duration_ms] > 100 then "text-warning"
|
357
|
+
else "text-success"
|
358
|
+
end %>
|
359
|
+
<span class="<%= duration_class %>">
|
360
|
+
<%= number_with_precision(query[:duration_ms], precision: 2) %> ms
|
361
|
+
</span>
|
362
|
+
</td>
|
363
|
+
<td>
|
364
|
+
<span class="small request-id">
|
365
|
+
<%= query[:request_id] %>
|
366
|
+
</span>
|
367
|
+
</td>
|
368
|
+
<td><%= query[:name] %></td>
|
369
|
+
<td>
|
370
|
+
<pre class="mb-0 sql-query rounded p-2 sql-code-block"><code class="syntax-highlighted"><%= query[:sql] %></code></pre>
|
371
|
+
<% if query[:binds].present? %>
|
372
|
+
<details class="mt-1 small">
|
373
|
+
<summary class="query-binds-summary">Binds</summary>
|
374
|
+
<code class="query-binds p-1 rounded d-inline-block"><%= query[:binds].inspect %></code>
|
375
|
+
</details>
|
376
|
+
<% end %>
|
377
|
+
</td>
|
378
|
+
</tr>
|
379
|
+
<% end %>
|
380
|
+
<% else %>
|
381
|
+
<tr>
|
382
|
+
<td colspan="6" class="text-center py-5 empty-data-message">
|
383
|
+
<i class="bi bi-database-x fs-2 d-block mb-2"></i>
|
384
|
+
No SQL queries logged yet.
|
385
|
+
<p class="mt-2">Navigate through the application to see queries being logged here.</p>
|
386
|
+
</td>
|
387
|
+
</tr>
|
388
|
+
<% end %>
|
389
|
+
</tbody>
|
390
|
+
</table>
|
391
|
+
</div>
|
392
|
+
</div>
|
393
|
+
</div>
|
394
|
+
</div>
|
395
|
+
|
396
|
+
<% if @stats[:tables_queried].present? %>
|
397
|
+
<script>
|
398
|
+
document.addEventListener('DOMContentLoaded', function() {
|
399
|
+
const ctx = document.getElementById('tablesChart').getContext('2d');
|
400
|
+
const tableData = <%= raw @stats[:tables_queried].to_json %>;
|
401
|
+
const labels = Object.keys(tableData);
|
402
|
+
const values = Object.values(tableData);
|
403
|
+
|
404
|
+
// Get theme-compatible colors
|
405
|
+
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
|
406
|
+
const chartBgColor = isDarkMode ? 'rgba(75, 192, 192, 0.4)' : 'rgba(75, 192, 192, 0.6)';
|
407
|
+
const chartBorderColor = isDarkMode ? 'rgba(75, 192, 192, 0.8)' : 'rgba(75, 192, 192, 1)';
|
408
|
+
|
409
|
+
const chart = new Chart(ctx, {
|
410
|
+
type: 'bar',
|
411
|
+
data: {
|
412
|
+
labels: labels,
|
413
|
+
datasets: [{
|
414
|
+
label: 'Query Count',
|
415
|
+
data: values,
|
416
|
+
backgroundColor: chartBgColor,
|
417
|
+
borderColor: chartBorderColor,
|
418
|
+
borderWidth: 1
|
419
|
+
}]
|
420
|
+
},
|
421
|
+
options: {
|
422
|
+
scales: {
|
423
|
+
y: {
|
424
|
+
beginAtZero: true,
|
425
|
+
ticks: {
|
426
|
+
precision: 0,
|
427
|
+
color: getComputedStyle(document.documentElement).getPropertyValue('--bs-body-color')
|
428
|
+
},
|
429
|
+
grid: {
|
430
|
+
color: getComputedStyle(document.documentElement).getPropertyValue('--bs-border-color-translucent')
|
431
|
+
}
|
432
|
+
},
|
433
|
+
x: {
|
434
|
+
ticks: {
|
435
|
+
color: getComputedStyle(document.documentElement).getPropertyValue('--bs-body-color')
|
436
|
+
},
|
437
|
+
grid: {
|
438
|
+
color: getComputedStyle(document.documentElement).getPropertyValue('--bs-border-color-translucent')
|
439
|
+
}
|
440
|
+
}
|
441
|
+
},
|
442
|
+
plugins: {
|
443
|
+
legend: {
|
444
|
+
display: false,
|
445
|
+
labels: {
|
446
|
+
color: getComputedStyle(document.documentElement).getPropertyValue('--bs-body-color')
|
447
|
+
}
|
448
|
+
},
|
449
|
+
title: {
|
450
|
+
display: true,
|
451
|
+
text: 'Number of Queries by Table',
|
452
|
+
color: getComputedStyle(document.documentElement).getPropertyValue('--bs-body-color')
|
453
|
+
},
|
454
|
+
tooltip: {
|
455
|
+
callbacks: {
|
456
|
+
label: function(context) {
|
457
|
+
return `${context.parsed.y} queries`;
|
458
|
+
}
|
459
|
+
}
|
460
|
+
}
|
461
|
+
}
|
462
|
+
});
|
463
|
+
|
464
|
+
// Update chart when theme changes
|
465
|
+
document.addEventListener('dbviewerThemeChanged', function(e) {
|
466
|
+
const isDarkMode = e.detail.theme === 'dark';
|
467
|
+
const chartBgColor = isDarkMode ? 'rgba(75, 192, 192, 0.4)' : 'rgba(75, 192, 192, 0.6)';
|
468
|
+
const chartBorderColor = isDarkMode ? 'rgba(75, 192, 192, 0.8)' : 'rgba(75, 192, 192, 1)';
|
469
|
+
|
470
|
+
chart.data.datasets[0].backgroundColor = chartBgColor;
|
471
|
+
chart.data.datasets[0].borderColor = chartBorderColor;
|
472
|
+
|
473
|
+
// Update text colors
|
474
|
+
chart.options.scales.y.ticks.color = getComputedStyle(document.documentElement).getPropertyValue('--bs-body-color');
|
475
|
+
chart.options.scales.x.ticks.color = getComputedStyle(document.documentElement).getPropertyValue('--bs-body-color');
|
476
|
+
chart.options.scales.y.grid.color = getComputedStyle(document.documentElement).getPropertyValue('--bs-border-color-translucent');
|
477
|
+
chart.options.scales.x.grid.color = getComputedStyle(document.documentElement).getPropertyValue('--bs-border-color-translucent');
|
478
|
+
chart.options.plugins.title.color = getComputedStyle(document.documentElement).getPropertyValue('--bs-body-color');
|
479
|
+
|
480
|
+
chart.update();
|
481
|
+
});
|
482
|
+
});
|
483
|
+
</script>
|
484
|
+
<% end %>
|
485
|
+
|
486
|
+
<style>
|
487
|
+
.sql-query {
|
488
|
+
white-space: pre-wrap;
|
489
|
+
word-wrap: break-word;
|
490
|
+
max-height: 100px;
|
491
|
+
overflow-y: auto;
|
492
|
+
font-size: 0.85rem;
|
493
|
+
background-color: #f8f9fa;
|
494
|
+
border-radius: 3px;
|
495
|
+
padding: 0.5rem;
|
496
|
+
}
|
497
|
+
|
498
|
+
.request-group-header {
|
499
|
+
border-top: 2px solid #dee2e6;
|
500
|
+
}
|
501
|
+
|
502
|
+
.request-group-header:first-child {
|
503
|
+
border-top: none;
|
504
|
+
}
|
505
|
+
|
506
|
+
details summary {
|
507
|
+
cursor: pointer;
|
508
|
+
}
|
509
|
+
|
510
|
+
.cursor-pointer {
|
511
|
+
cursor: pointer;
|
512
|
+
}
|
513
|
+
|
514
|
+
/* Rotate icon when expanded */
|
515
|
+
.collapsed-section[aria-expanded="true"] .bi-chevron-down {
|
516
|
+
transform: rotate(180deg);
|
517
|
+
transition: transform 0.3s ease;
|
518
|
+
}
|
519
|
+
|
520
|
+
.collapsed-section[aria-expanded="false"] .bi-chevron-down {
|
521
|
+
transform: rotate(0deg);
|
522
|
+
transition: transform 0.3s ease;
|
523
|
+
}
|
524
|
+
</style>
|
525
|
+
|
526
|
+
<style>
|
527
|
+
/* SQL query styling with dark mode support */
|
528
|
+
.sql-query {
|
529
|
+
white-space: pre-wrap;
|
530
|
+
word-wrap: break-word;
|
531
|
+
max-height: 100px;
|
532
|
+
overflow-y: auto;
|
533
|
+
font-size: 0.85rem;
|
534
|
+
border-radius: 3px;
|
535
|
+
padding: 0.5rem;
|
536
|
+
}
|
537
|
+
|
538
|
+
/* Dark mode compatible styling */
|
539
|
+
.sql-code-block {
|
540
|
+
background-color: var(--bs-secondary-bg);
|
541
|
+
}
|
542
|
+
|
543
|
+
[data-bs-theme="dark"] .sql-code-block {
|
544
|
+
background-color: var(--bs-tertiary-bg);
|
545
|
+
}
|
546
|
+
|
547
|
+
/* Request group header styling */
|
548
|
+
.request-group-header {
|
549
|
+
border-top: 2px solid var(--bs-border-color);
|
550
|
+
}
|
551
|
+
|
552
|
+
.request-header-bg {
|
553
|
+
background-color: var(--bs-secondary-bg);
|
554
|
+
}
|
555
|
+
|
556
|
+
.request-group-header:first-child {
|
557
|
+
border-top: none;
|
558
|
+
}
|
559
|
+
|
560
|
+
/* Collapsed section styling */
|
561
|
+
details summary {
|
562
|
+
cursor: pointer;
|
563
|
+
}
|
564
|
+
|
565
|
+
.cursor-pointer {
|
566
|
+
cursor: pointer;
|
567
|
+
}
|
568
|
+
|
569
|
+
/* Rotate icon when expanded */
|
570
|
+
.collapsed-section[aria-expanded="true"] .bi-chevron-down {
|
571
|
+
transform: rotate(180deg);
|
572
|
+
transition: transform 0.3s ease;
|
573
|
+
}
|
574
|
+
|
575
|
+
.collapsed-section[aria-expanded="false"] .bi-chevron-down {
|
576
|
+
transform: rotate(0deg);
|
577
|
+
transition: transform 0.3s ease;
|
578
|
+
}
|
579
|
+
|
580
|
+
/* Code styling */
|
581
|
+
.pattern-code, .query-binds {
|
582
|
+
background-color: var(--bs-secondary-bg);
|
583
|
+
border: 1px solid var(--bs-border-color);
|
584
|
+
}
|
585
|
+
|
586
|
+
/* Empty data message */
|
587
|
+
.empty-data-message {
|
588
|
+
color: var(--bs-secondary-color);
|
589
|
+
}
|
590
|
+
</style>
|
591
|
+
|
592
|
+
<script>
|
593
|
+
document.addEventListener('DOMContentLoaded', function() {
|
594
|
+
// Add Bootstrap collapse event handlers to animate icons
|
595
|
+
const n1CollapseTrigger = document.querySelector('[data-bs-target="#n1QueriesCollapse"]');
|
596
|
+
const slowestCollapseTrigger = document.querySelector('[data-bs-target="#slowestQueriesCollapse"]');
|
597
|
+
|
598
|
+
if (n1CollapseTrigger) {
|
599
|
+
n1CollapseTrigger.classList.add('collapsed-section');
|
600
|
+
n1CollapseTrigger.addEventListener('click', function() {
|
601
|
+
const isExpanded = this.getAttribute('aria-expanded') === 'true';
|
602
|
+
this.setAttribute('aria-expanded', !isExpanded);
|
603
|
+
});
|
604
|
+
}
|
605
|
+
|
606
|
+
if (slowestCollapseTrigger) {
|
607
|
+
slowestCollapseTrigger.classList.add('collapsed-section');
|
608
|
+
slowestCollapseTrigger.addEventListener('click', function() {
|
609
|
+
const isExpanded = this.getAttribute('aria-expanded') === 'true';
|
610
|
+
this.setAttribute('aria-expanded', !isExpanded);
|
611
|
+
});
|
612
|
+
}
|
613
|
+
});
|
614
|
+
</script>
|