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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +250 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/dbviewer/application.css +21 -0
  6. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  7. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  8. data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
  9. data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
  10. data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
  11. data/app/controllers/dbviewer/application_controller.rb +21 -0
  12. data/app/controllers/dbviewer/databases_controller.rb +0 -0
  13. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
  14. data/app/controllers/dbviewer/home_controller.rb +10 -0
  15. data/app/controllers/dbviewer/logs_controller.rb +39 -0
  16. data/app/controllers/dbviewer/tables_controller.rb +73 -0
  17. data/app/helpers/dbviewer/application_helper.rb +118 -0
  18. data/app/jobs/dbviewer/application_job.rb +4 -0
  19. data/app/mailers/dbviewer/application_mailer.rb +6 -0
  20. data/app/models/dbviewer/application_record.rb +5 -0
  21. data/app/services/dbviewer/file_storage.rb +0 -0
  22. data/app/services/dbviewer/in_memory_storage.rb +0 -0
  23. data/app/services/dbviewer/query_analyzer.rb +0 -0
  24. data/app/services/dbviewer/query_collection.rb +0 -0
  25. data/app/services/dbviewer/query_logger.rb +0 -0
  26. data/app/services/dbviewer/query_parser.rb +82 -0
  27. data/app/services/dbviewer/query_storage.rb +0 -0
  28. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
  29. data/app/views/dbviewer/home/index.html.erb +237 -0
  30. data/app/views/dbviewer/logs/index.html.erb +614 -0
  31. data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
  32. data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
  33. data/app/views/dbviewer/tables/index.html.erb +128 -0
  34. data/app/views/dbviewer/tables/query.html.erb +600 -0
  35. data/app/views/dbviewer/tables/show.html.erb +271 -0
  36. data/app/views/layouts/dbviewer/application.html.erb +728 -0
  37. data/config/routes.rb +22 -0
  38. data/lib/dbviewer/configuration.rb +79 -0
  39. data/lib/dbviewer/database_manager.rb +450 -0
  40. data/lib/dbviewer/engine.rb +20 -0
  41. data/lib/dbviewer/initializer.rb +23 -0
  42. data/lib/dbviewer/logger.rb +102 -0
  43. data/lib/dbviewer/query_analyzer.rb +109 -0
  44. data/lib/dbviewer/query_collection.rb +41 -0
  45. data/lib/dbviewer/query_parser.rb +82 -0
  46. data/lib/dbviewer/sql_validator.rb +194 -0
  47. data/lib/dbviewer/storage/base.rb +31 -0
  48. data/lib/dbviewer/storage/file_storage.rb +96 -0
  49. data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
  50. data/lib/dbviewer/version.rb +3 -0
  51. data/lib/dbviewer.rb +65 -0
  52. data/lib/tasks/dbviewer_tasks.rake +4 -0
  53. 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>