pg_reports 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +23 -2
- data/app/controllers/pg_reports/dashboard_controller.rb +241 -24
- data/app/views/layouts/pg_reports/application.html.erb +616 -201
- data/app/views/pg_reports/dashboard/_database_selector.html.erb +2 -1
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +10 -7
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +100 -51
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +392 -476
- data/app/views/pg_reports/dashboard/index.html.erb +426 -335
- data/app/views/pg_reports/dashboard/show.html.erb +13 -11
- data/bin/pg_reports +28 -2
- data/config/brakeman.ignore +97 -0
- data/config/locales/en.yml +62 -47
- data/config/locales/ru.yml +62 -47
- data/config/locales/uk.yml +62 -47
- data/config/routes.rb +1 -0
- data/lib/pg_reports/configuration.rb +8 -0
- data/lib/pg_reports/connection/error_translator.rb +43 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +10 -0
- data/lib/pg_reports/explain_analyzer.rb +0 -3
- data/lib/pg_reports/grafana/exporter.rb +5 -1
- data/lib/pg_reports/query_monitor.rb +15 -3
- data/lib/pg_reports/standalone.rb +59 -1
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +5 -0
- metadata +2 -1
|
@@ -12,23 +12,25 @@
|
|
|
12
12
|
|
|
13
13
|
<header class="header">
|
|
14
14
|
<div class="logo">
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
</h1>
|
|
21
|
-
<span class="logo-subtitle"><%= t("pg_reports.ui.branding.subtitle") %></span>
|
|
22
|
-
</div>
|
|
15
|
+
<h1>
|
|
16
|
+
<%= t("pg_reports.ui.branding.title") %>
|
|
17
|
+
<span class="logo-version">v<%= PgReports::VERSION %></span>
|
|
18
|
+
</h1>
|
|
19
|
+
<span class="logo-subtitle"><%= t("pg_reports.ui.branding.subtitle") %></span>
|
|
23
20
|
</div>
|
|
24
21
|
|
|
25
22
|
<div class="header-actions">
|
|
23
|
+
<button class="btn btn-primary" onclick="showRunQueryModal()" id="run-query-open-btn">
|
|
24
|
+
<%= t("pg_reports.ui.actions.run_query") %>
|
|
25
|
+
</button>
|
|
26
26
|
<% if @pg_stat_status[:ready] %>
|
|
27
|
-
<button class="btn btn-
|
|
27
|
+
<button class="btn btn-muted" onclick="showResetConfirmModal()" id="reset-btn">
|
|
28
28
|
<%= t("pg_reports.ui.actions.reset_statistics") %>
|
|
29
29
|
</button>
|
|
30
30
|
<% end %>
|
|
31
|
-
<button class="btn-
|
|
31
|
+
<button type="button" class="btn-icon" onclick="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>" aria-label="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>">
|
|
32
|
+
<svg class="icon icon-lg"><use href="#i-settings"></use></svg>
|
|
33
|
+
</button>
|
|
32
34
|
|
|
33
35
|
<% if !@pg_stat_status[:connected] %>
|
|
34
36
|
<div class="header-badge" id="pg-stat-badge">
|
|
@@ -45,14 +47,14 @@
|
|
|
45
47
|
onclick="showPgStatInfo()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
|
|
46
48
|
<span class="badge-dot warning"></span>
|
|
47
49
|
<span><%= t("pg_reports.ui.status.not_preloaded") %></span>
|
|
48
|
-
<
|
|
50
|
+
<svg class="icon badge-info-icon" aria-hidden="true"><use href="#i-info"></use></svg>
|
|
49
51
|
</button>
|
|
50
52
|
<% else %>
|
|
51
53
|
<button type="button" class="header-badge header-badge-clickable" id="pg-stat-badge"
|
|
52
54
|
onclick="showCreateExtensionModal()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
|
|
53
55
|
<span class="badge-dot warning"></span>
|
|
54
56
|
<span><%= t("pg_reports.ui.status.extension_missing") %></span>
|
|
55
|
-
<
|
|
57
|
+
<svg class="icon badge-info-icon" aria-hidden="true"><use href="#i-info"></use></svg>
|
|
56
58
|
</button>
|
|
57
59
|
<% end %>
|
|
58
60
|
</div>
|
|
@@ -167,6 +169,40 @@ pg_stat_statements.track = all</pre>
|
|
|
167
169
|
</div>
|
|
168
170
|
</div>
|
|
169
171
|
|
|
172
|
+
<!-- Run Query Modal -->
|
|
173
|
+
<div id="run-query-modal" class="modal" style="display: none;">
|
|
174
|
+
<div class="modal-content modal-run-query">
|
|
175
|
+
<div class="modal-header">
|
|
176
|
+
<h3><%= t("pg_reports.ui.modals.run_query_title") %></h3>
|
|
177
|
+
<button class="modal-close" onclick="closeRunQueryModal()">×</button>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="modal-body">
|
|
180
|
+
<% if PgReports.config.allow_raw_query_execution %>
|
|
181
|
+
<label class="explain-label" for="run-query-input"><%= t("pg_reports.ui.modals.query_label") %></label>
|
|
182
|
+
<textarea id="run-query-input" class="run-query-textarea" spellcheck="false" placeholder="<%= t("pg_reports.ui.modals.run_query_placeholder") %>"></textarea>
|
|
183
|
+
<div class="run-query-actions">
|
|
184
|
+
<button class="btn btn-secondary" onclick="clearRunQuery()"><%= t("pg_reports.ui.actions.clear_all") %></button>
|
|
185
|
+
<button class="btn btn-primary" onclick="executeRunQuery()" id="btn-run-query"><%= t("pg_reports.ui.actions.run_query") %></button>
|
|
186
|
+
</div>
|
|
187
|
+
<div id="run-query-loading" class="loading" style="display: none;">
|
|
188
|
+
<div class="spinner"></div>
|
|
189
|
+
</div>
|
|
190
|
+
<div id="run-query-content"></div>
|
|
191
|
+
<% else %>
|
|
192
|
+
<div class="error-message">
|
|
193
|
+
<p class="error-title"><%= t("pg_reports.ui.modals.query_execution_disabled_title") %></p>
|
|
194
|
+
<p><%= t("pg_reports.ui.modals.query_execution_disabled_intro") %></p>
|
|
195
|
+
<pre class="code-block">PgReports.configure do |config|
|
|
196
|
+
config.allow_raw_query_execution = true
|
|
197
|
+
end</pre>
|
|
198
|
+
<p><%= t("pg_reports.ui.modals.query_execution_disabled_env_note") %></p>
|
|
199
|
+
<pre class="code-block">PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION=true</pre>
|
|
200
|
+
</div>
|
|
201
|
+
<% end %>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
170
206
|
<!-- Live Monitoring Panel -->
|
|
171
207
|
<div id="live-monitoring" class="live-monitoring-panel" style="display: none;">
|
|
172
208
|
<div class="live-monitoring-header">
|
|
@@ -178,8 +214,8 @@ pg_stat_statements.track = all</pre>
|
|
|
178
214
|
</div>
|
|
179
215
|
<div class="live-monitoring-controls">
|
|
180
216
|
<span class="live-monitoring-interval"><%= t("pg_reports.ui.monitoring.update_interval") %></span>
|
|
181
|
-
<button class="btn-
|
|
182
|
-
<
|
|
217
|
+
<button type="button" class="btn-icon btn-small" onclick="toggleLiveMonitoring()" title="<%= t("pg_reports.ui.monitoring.toggle_title") %>" aria-label="<%= t("pg_reports.ui.monitoring.toggle_title") %>">
|
|
218
|
+
<svg class="icon" id="toggle-icon"><use href="#i-pause"></use></svg>
|
|
183
219
|
</button>
|
|
184
220
|
</div>
|
|
185
221
|
</div>
|
|
@@ -188,7 +224,7 @@ pg_stat_statements.track = all</pre>
|
|
|
188
224
|
<!-- Connections -->
|
|
189
225
|
<div class="live-metric-card" data-metric="connections">
|
|
190
226
|
<div class="metric-header">
|
|
191
|
-
<
|
|
227
|
+
<svg class="icon metric-icon"><use href="#i-link"></use></svg>
|
|
192
228
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.connections_label") %></span>
|
|
193
229
|
</div>
|
|
194
230
|
<div class="metric-body">
|
|
@@ -209,7 +245,7 @@ pg_stat_statements.track = all</pre>
|
|
|
209
245
|
<!-- TPS -->
|
|
210
246
|
<div class="live-metric-card" data-metric="tps">
|
|
211
247
|
<div class="metric-header">
|
|
212
|
-
<
|
|
248
|
+
<svg class="icon metric-icon"><use href="#i-activity"></use></svg>
|
|
213
249
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.tps_label") %></span>
|
|
214
250
|
</div>
|
|
215
251
|
<div class="metric-body">
|
|
@@ -230,7 +266,7 @@ pg_stat_statements.track = all</pre>
|
|
|
230
266
|
<!-- Cache Hit Ratio -->
|
|
231
267
|
<div class="live-metric-card" data-metric="cache">
|
|
232
268
|
<div class="metric-header">
|
|
233
|
-
<
|
|
269
|
+
<svg class="icon metric-icon"><use href="#i-database"></use></svg>
|
|
234
270
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.cache_hit_label") %></span>
|
|
235
271
|
</div>
|
|
236
272
|
<div class="metric-body">
|
|
@@ -249,7 +285,7 @@ pg_stat_statements.track = all</pre>
|
|
|
249
285
|
<!-- Long Running Queries -->
|
|
250
286
|
<div class="live-metric-card" data-metric="longrunning">
|
|
251
287
|
<div class="metric-header">
|
|
252
|
-
<
|
|
288
|
+
<svg class="icon metric-icon"><use href="#i-clock"></use></svg>
|
|
253
289
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.long_queries_label") %></span>
|
|
254
290
|
</div>
|
|
255
291
|
<div class="metric-body">
|
|
@@ -268,7 +304,7 @@ pg_stat_statements.track = all</pre>
|
|
|
268
304
|
<!-- Blocked Queries -->
|
|
269
305
|
<div class="live-metric-card" data-metric="blocked">
|
|
270
306
|
<div class="metric-header">
|
|
271
|
-
<
|
|
307
|
+
<svg class="icon metric-icon"><use href="#i-lock"></use></svg>
|
|
272
308
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.blocked_label") %></span>
|
|
273
309
|
</div>
|
|
274
310
|
<div class="metric-body">
|
|
@@ -286,7 +322,11 @@ pg_stat_statements.track = all</pre>
|
|
|
286
322
|
</div>
|
|
287
323
|
</div>
|
|
288
324
|
|
|
289
|
-
<!-- Query Monitoring Panel
|
|
325
|
+
<!-- Query Monitoring Panel: taps ActiveSupport::Notifications in the host
|
|
326
|
+
application's process. There is no host app in standalone mode, so the
|
|
327
|
+
panel is hidden entirely rather than showing a monitor with nothing to
|
|
328
|
+
monitor. -->
|
|
329
|
+
<% unless PgReports.config.standalone %>
|
|
290
330
|
<div id="query-monitoring" class="query-monitoring-panel" style="display: none;">
|
|
291
331
|
<div class="query-monitoring-header">
|
|
292
332
|
<div class="query-monitoring-title">
|
|
@@ -300,17 +340,17 @@ pg_stat_statements.track = all</pre>
|
|
|
300
340
|
</span>
|
|
301
341
|
</div>
|
|
302
342
|
<div class="query-monitoring-controls">
|
|
303
|
-
<button class="btn btn-
|
|
343
|
+
<button class="btn btn-primary" onclick="startQueryMonitoring(this)" id="start-monitor-btn">
|
|
304
344
|
<%= t("pg_reports.ui.actions.start_monitoring") %>
|
|
305
345
|
</button>
|
|
306
|
-
<button class="btn btn-
|
|
346
|
+
<button class="btn btn-danger" onclick="stopQueryMonitoring(this)" id="stop-monitor-btn" style="display: none;">
|
|
307
347
|
<%= t("pg_reports.ui.actions.stop_monitoring") %>
|
|
308
348
|
</button>
|
|
309
|
-
<button class="btn btn-
|
|
349
|
+
<button class="btn btn-secondary" onclick="loadQueryHistory(this)" id="load-history-btn" disabled>
|
|
310
350
|
<%= t("pg_reports.ui.actions.load_history") %>
|
|
311
351
|
</button>
|
|
312
352
|
<div class="download-dropdown" id="monitor-download-dropdown" style="display: none;">
|
|
313
|
-
<button class="btn btn-
|
|
353
|
+
<button class="btn btn-secondary" onclick="toggleMonitorDownloadMenu(event)">
|
|
314
354
|
<%= t("pg_reports.ui.actions.download") %>
|
|
315
355
|
</button>
|
|
316
356
|
<div class="download-menu" id="monitor-download-menu" style="display: none;">
|
|
@@ -331,35 +371,35 @@ pg_stat_statements.track = all</pre>
|
|
|
331
371
|
</div>
|
|
332
372
|
</div>
|
|
333
373
|
</div>
|
|
374
|
+
<% end %>
|
|
334
375
|
|
|
376
|
+
<%
|
|
377
|
+
# Sprite ids for the category glyphs defined in the layout. The registry's own
|
|
378
|
+
# :icon/:color keys stay untouched — they are part of its public shape.
|
|
379
|
+
category_icons = {
|
|
380
|
+
queries: "queries", indexes: "indexes", tables: "tables",
|
|
381
|
+
connections: "connections", system: "system", schema_analysis: "schema"
|
|
382
|
+
}
|
|
383
|
+
%>
|
|
335
384
|
<div class="categories-grid">
|
|
336
385
|
<% @categories.each do |category_key, category| %>
|
|
337
|
-
<%
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
is_disabled = requires_pg_stat || target_disabled_reason.present?
|
|
341
|
-
%>
|
|
342
|
-
<div class="category-card<%= ' disabled' if is_disabled %>">
|
|
343
|
-
<% if requires_pg_stat %>
|
|
344
|
-
<div class="category-warning">
|
|
345
|
-
<%= t("pg_reports.ui.categories.requires_pg_stat") %>
|
|
346
|
-
</div>
|
|
347
|
-
<% elsif target_disabled_reason %>
|
|
386
|
+
<% disabled_reason = category_disabled_reason(category_key) %>
|
|
387
|
+
<div class="category-card<%= ' disabled' if disabled_reason %>">
|
|
388
|
+
<% if disabled_reason %>
|
|
348
389
|
<div class="category-warning">
|
|
349
|
-
|
|
390
|
+
<svg class="icon" aria-hidden="true"><use href="#i-lock"></use></svg>
|
|
391
|
+
<span><%= disabled_reason %></span>
|
|
350
392
|
</div>
|
|
351
393
|
<% end %>
|
|
352
394
|
<div class="category-header">
|
|
353
|
-
<
|
|
354
|
-
<%= category[:icon] %>
|
|
355
|
-
</div>
|
|
395
|
+
<svg class="icon category-icon" aria-hidden="true"><use href="#i-<%= category_icons.fetch(category_key, "tables") %>"></use></svg>
|
|
356
396
|
<span class="category-title"><%= category[:name] %></span>
|
|
357
397
|
<span class="category-count"><%= category[:reports].size %> <%= t("pg_reports.ui.categories.reports_count_suffix") %></span>
|
|
358
398
|
</div>
|
|
359
399
|
|
|
360
400
|
<div class="reports-list">
|
|
361
401
|
<% category[:reports].each do |report_key, report| %>
|
|
362
|
-
<% if
|
|
402
|
+
<% if disabled_reason %>
|
|
363
403
|
<div class="report-link disabled">
|
|
364
404
|
<div class="report-link-info">
|
|
365
405
|
<span class="report-link-name">
|
|
@@ -368,7 +408,7 @@ pg_stat_statements.track = all</pre>
|
|
|
368
408
|
</span>
|
|
369
409
|
<span class="report-link-desc"><%= report[:description] %></span>
|
|
370
410
|
</div>
|
|
371
|
-
<
|
|
411
|
+
<svg class="icon lock" aria-hidden="true"><use href="#i-lock"></use></svg>
|
|
372
412
|
</div>
|
|
373
413
|
<% else %>
|
|
374
414
|
<%= link_to report_path(category: category_key, report: report_key), class: "report-link" do %>
|
|
@@ -379,7 +419,7 @@ pg_stat_statements.track = all</pre>
|
|
|
379
419
|
</span>
|
|
380
420
|
<span class="report-link-desc"><%= report[:description] %></span>
|
|
381
421
|
</div>
|
|
382
|
-
<
|
|
422
|
+
<svg class="icon arrow" aria-hidden="true"><use href="#i-chevron-right"></use></svg>
|
|
383
423
|
<% end %>
|
|
384
424
|
<% end %>
|
|
385
425
|
<% end %>
|
|
@@ -412,7 +452,7 @@ pg_stat_statements.track = all</pre>
|
|
|
412
452
|
text-decoration: none;
|
|
413
453
|
}
|
|
414
454
|
.dashboard-footer a:hover {
|
|
415
|
-
color: var(--
|
|
455
|
+
color: var(--text-primary);
|
|
416
456
|
text-decoration: underline;
|
|
417
457
|
}
|
|
418
458
|
.dashboard-footer .footer-sep {
|
|
@@ -425,104 +465,45 @@ pg_stat_statements.track = all</pre>
|
|
|
425
465
|
gap: 0.5rem;
|
|
426
466
|
}
|
|
427
467
|
|
|
428
|
-
.btn.btn-small {
|
|
429
|
-
padding: 0.5rem 1rem;
|
|
430
|
-
font-size: 0.8rem;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/* Keep the header action button the same height as the status badge and
|
|
434
|
-
the settings button (all 2.125rem); inline-flex centers the label so no
|
|
435
|
-
vertical padding is needed. */
|
|
436
|
-
.header-actions .btn {
|
|
437
|
-
height: 2.125rem;
|
|
438
|
-
padding: 0 1rem;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
.btn-info {
|
|
442
|
-
width: 2.125rem;
|
|
443
|
-
height: 2.125rem;
|
|
444
|
-
padding: 0;
|
|
445
|
-
display: inline-flex;
|
|
446
|
-
align-items: center;
|
|
447
|
-
justify-content: center;
|
|
448
|
-
background: var(--bg-tertiary);
|
|
449
|
-
color: var(--text-muted);
|
|
450
|
-
border: 1px solid var(--border-color);
|
|
451
|
-
border-radius: 4px;
|
|
452
|
-
font-size: 0.9rem;
|
|
453
|
-
font-weight: 600;
|
|
454
|
-
cursor: pointer;
|
|
455
|
-
transition: all 0.15s;
|
|
456
|
-
}
|
|
457
|
-
.btn-info:hover {
|
|
458
|
-
background: var(--accent-purple);
|
|
459
|
-
color: white;
|
|
460
|
-
border-color: var(--accent-purple);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
.btn.btn-danger {
|
|
464
|
-
background: var(--accent-rose);
|
|
465
|
-
border-color: var(--accent-rose);
|
|
466
|
-
}
|
|
467
|
-
.btn.btn-danger:hover {
|
|
468
|
-
background: #c45a6e;
|
|
469
|
-
border-color: #c45a6e;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
.btn.btn-secondary {
|
|
473
|
-
background: var(--bg-tertiary);
|
|
474
|
-
border-color: var(--border-color);
|
|
475
|
-
color: var(--text-secondary);
|
|
476
|
-
}
|
|
477
|
-
.btn.btn-secondary:hover {
|
|
478
|
-
background: var(--bg-card);
|
|
479
|
-
color: var(--text-primary);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
.btn.btn-muted {
|
|
483
|
-
background: var(--bg-tertiary);
|
|
484
|
-
border-color: var(--accent-rose);
|
|
485
|
-
color: var(--text-secondary);
|
|
486
|
-
}
|
|
487
|
-
.btn.btn-muted:hover {
|
|
488
|
-
background: var(--bg-card);
|
|
489
|
-
color: var(--text-primary);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
468
|
.badge-dot.error { background: var(--accent-rose); }
|
|
493
469
|
|
|
494
470
|
.category-card.disabled {
|
|
495
471
|
opacity: 0.7;
|
|
496
472
|
}
|
|
497
473
|
|
|
474
|
+
/* Why a category is unavailable. The icon carries the state; the reason
|
|
475
|
+
itself is body copy, not a wall of amber. */
|
|
498
476
|
.category-warning {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: flex-start;
|
|
479
|
+
gap: 0.45rem;
|
|
480
|
+
padding: 0.5rem 1rem;
|
|
481
|
+
margin: -1rem -1rem 1rem;
|
|
482
|
+
background: var(--bg-tertiary);
|
|
483
|
+
border-bottom: 1px solid var(--border-color);
|
|
484
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
485
|
+
color: var(--text-secondary);
|
|
486
|
+
font-size: 0.75rem;
|
|
487
|
+
line-height: 1.5;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.category-warning .icon {
|
|
491
|
+
width: 0.8125rem;
|
|
492
|
+
height: 0.8125rem;
|
|
493
|
+
margin-top: 0.25rem;
|
|
504
494
|
color: var(--accent-amber);
|
|
505
|
-
font-size: 0.8rem;
|
|
506
|
-
font-weight: 500;
|
|
507
|
-
text-align: center;
|
|
508
495
|
}
|
|
509
496
|
|
|
510
497
|
.report-link.disabled {
|
|
511
|
-
|
|
512
|
-
align-items: center;
|
|
513
|
-
justify-content: space-between;
|
|
514
|
-
padding: 0.75rem 1rem;
|
|
515
|
-
background: var(--bg-tertiary);
|
|
516
|
-
border: 1px solid transparent;
|
|
517
|
-
border-radius: 6px;
|
|
498
|
+
background: transparent;
|
|
518
499
|
color: var(--text-muted);
|
|
519
|
-
font-size: 0.9rem;
|
|
520
500
|
cursor: not-allowed;
|
|
521
501
|
}
|
|
522
502
|
|
|
523
503
|
.report-link .lock {
|
|
524
|
-
|
|
525
|
-
|
|
504
|
+
width: 0.8125rem;
|
|
505
|
+
height: 0.8125rem;
|
|
506
|
+
opacity: 0.6;
|
|
526
507
|
}
|
|
527
508
|
|
|
528
509
|
.report-link-info {
|
|
@@ -550,66 +531,8 @@ pg_stat_statements.track = all</pre>
|
|
|
550
531
|
color: var(--text-secondary);
|
|
551
532
|
}
|
|
552
533
|
|
|
553
|
-
/* Modal */
|
|
554
|
-
.modal {
|
|
555
|
-
position: fixed;
|
|
556
|
-
inset: 0;
|
|
557
|
-
background: rgba(0, 0, 0, 0.7);
|
|
558
|
-
display: flex;
|
|
559
|
-
align-items: center;
|
|
560
|
-
justify-content: center;
|
|
561
|
-
z-index: 1000;
|
|
562
|
-
backdrop-filter: blur(4px);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
.modal-content {
|
|
566
|
-
background: var(--bg-card);
|
|
567
|
-
border: 1px solid var(--border-color);
|
|
568
|
-
border-radius: 6px;
|
|
569
|
-
max-width: 600px;
|
|
570
|
-
width: 90%;
|
|
571
|
-
max-height: 80vh;
|
|
572
|
-
overflow: auto;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
.modal-header {
|
|
576
|
-
display: flex;
|
|
577
|
-
align-items: center;
|
|
578
|
-
justify-content: space-between;
|
|
579
|
-
padding: 1.25rem 1.5rem;
|
|
580
|
-
border-bottom: 1px solid var(--border-color);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
.modal-header h3 {
|
|
584
|
-
font-size: 1.125rem;
|
|
585
|
-
font-weight: 600;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
.modal-close {
|
|
589
|
-
width: 32px;
|
|
590
|
-
height: 32px;
|
|
591
|
-
background: var(--bg-tertiary);
|
|
592
|
-
border: 1px solid var(--border-color);
|
|
593
|
-
border-radius: 8px;
|
|
594
|
-
color: var(--text-muted);
|
|
595
|
-
font-size: 1.25rem;
|
|
596
|
-
cursor: pointer;
|
|
597
|
-
transition: all 0.15s;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
.modal-close:hover {
|
|
601
|
-
background: var(--accent-rose);
|
|
602
|
-
border-color: var(--accent-rose);
|
|
603
|
-
color: white;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
.modal-body {
|
|
607
|
-
padding: 1.5rem;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
534
|
.modal-body p {
|
|
611
535
|
color: var(--text-secondary);
|
|
612
|
-
margin-bottom: 1rem;
|
|
613
536
|
}
|
|
614
537
|
|
|
615
538
|
.modal-body ol {
|
|
@@ -617,33 +540,14 @@ pg_stat_statements.track = all</pre>
|
|
|
617
540
|
color: var(--text-secondary);
|
|
618
541
|
}
|
|
619
542
|
|
|
620
|
-
.modal-body li {
|
|
621
|
-
margin-bottom: 1.25rem;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
543
|
.modal-body strong {
|
|
625
544
|
color: var(--text-primary);
|
|
626
545
|
}
|
|
627
546
|
|
|
628
|
-
|
|
547
|
+
/* A step's label and its snippet are one unit — tighter than the gap
|
|
548
|
+
between steps. */
|
|
549
|
+
.modal-body li > strong + pre {
|
|
629
550
|
margin-top: 0.5rem;
|
|
630
|
-
padding: 1rem;
|
|
631
|
-
background: var(--bg-primary);
|
|
632
|
-
border: 1px solid var(--border-color);
|
|
633
|
-
border-radius: 8px;
|
|
634
|
-
font-family: 'JetBrains Mono', monospace;
|
|
635
|
-
font-size: 0.8rem;
|
|
636
|
-
color: var(--accent-green);
|
|
637
|
-
overflow-x: auto;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
.modal-small {
|
|
641
|
-
max-width: 420px;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
.modal-header-danger {
|
|
645
|
-
background: rgba(244, 63, 94, 0.1);
|
|
646
|
-
border-bottom-color: rgba(244, 63, 94, 0.2);
|
|
647
551
|
}
|
|
648
552
|
|
|
649
553
|
.modal-header-danger h3 {
|
|
@@ -651,58 +555,55 @@ pg_stat_statements.track = all</pre>
|
|
|
651
555
|
}
|
|
652
556
|
|
|
653
557
|
.warning-text {
|
|
654
|
-
font-size:
|
|
558
|
+
font-size: 0.9375rem;
|
|
655
559
|
font-weight: 500;
|
|
656
|
-
color: var(--text-primary)
|
|
657
|
-
margin-bottom: 0.5rem !important;
|
|
560
|
+
color: var(--text-primary);
|
|
658
561
|
}
|
|
659
562
|
|
|
660
563
|
.warning-subtext {
|
|
661
|
-
font-size: 0.
|
|
662
|
-
color: var(--text-muted)
|
|
663
|
-
margin-bottom: 1.5rem !important;
|
|
564
|
+
font-size: 0.8125rem;
|
|
565
|
+
color: var(--text-muted);
|
|
664
566
|
}
|
|
665
567
|
|
|
666
568
|
.modal-actions {
|
|
667
569
|
display: flex;
|
|
668
|
-
gap: 0.
|
|
570
|
+
gap: 0.5rem;
|
|
669
571
|
justify-content: flex-end;
|
|
670
572
|
}
|
|
671
573
|
|
|
672
|
-
.modal-actions .btn {
|
|
673
|
-
padding: 0.625rem 1.25rem;
|
|
674
|
-
font-size: 0.875rem;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
574
|
/* Live Monitoring Panel */
|
|
678
575
|
.live-monitoring-panel {
|
|
679
|
-
margin-bottom:
|
|
576
|
+
margin-bottom: 1.5rem;
|
|
680
577
|
background: var(--bg-card);
|
|
681
578
|
border: 1px solid var(--border-color);
|
|
682
|
-
border-radius:
|
|
683
|
-
padding:
|
|
579
|
+
border-radius: var(--radius);
|
|
580
|
+
padding: 0.875rem 1rem;
|
|
684
581
|
}
|
|
685
582
|
|
|
686
583
|
.live-monitoring-header {
|
|
687
584
|
display: flex;
|
|
688
585
|
align-items: center;
|
|
689
586
|
justify-content: space-between;
|
|
690
|
-
|
|
691
|
-
|
|
587
|
+
gap: 1rem;
|
|
588
|
+
flex-wrap: wrap;
|
|
589
|
+
margin-bottom: 0.875rem;
|
|
590
|
+
padding-bottom: 0.75rem;
|
|
692
591
|
border-bottom: 1px solid var(--border-color);
|
|
693
592
|
}
|
|
694
593
|
|
|
695
594
|
.live-monitoring-title {
|
|
696
595
|
display: flex;
|
|
697
596
|
align-items: center;
|
|
698
|
-
gap: 0.
|
|
597
|
+
gap: 0.625rem;
|
|
699
598
|
font-weight: 600;
|
|
700
|
-
font-size:
|
|
599
|
+
font-size: 0.875rem;
|
|
600
|
+
min-width: 0;
|
|
701
601
|
}
|
|
702
602
|
|
|
703
603
|
.live-indicator {
|
|
704
|
-
width:
|
|
705
|
-
height:
|
|
604
|
+
width: 7px;
|
|
605
|
+
height: 7px;
|
|
606
|
+
flex: none;
|
|
706
607
|
border-radius: 50%;
|
|
707
608
|
background: var(--accent-green);
|
|
708
609
|
animation: pulse 2s infinite;
|
|
@@ -724,29 +625,11 @@ pg_stat_statements.track = all</pre>
|
|
|
724
625
|
color: var(--text-muted);
|
|
725
626
|
}
|
|
726
627
|
|
|
727
|
-
.btn-toggle-live {
|
|
728
|
-
width: 32px;
|
|
729
|
-
height: 32px;
|
|
730
|
-
padding: 0;
|
|
731
|
-
background: var(--bg-tertiary);
|
|
732
|
-
border: 1px solid var(--border-color);
|
|
733
|
-
border-radius: 8px;
|
|
734
|
-
color: var(--text-muted);
|
|
735
|
-
font-size: 0.875rem;
|
|
736
|
-
cursor: pointer;
|
|
737
|
-
transition: all 0.15s;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
.btn-toggle-live:hover {
|
|
741
|
-
background: var(--bg-secondary);
|
|
742
|
-
color: var(--text-primary);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
628
|
/* Metrics Grid */
|
|
746
629
|
.live-metrics-grid {
|
|
747
630
|
display: grid;
|
|
748
631
|
grid-template-columns: repeat(5, 1fr);
|
|
749
|
-
gap:
|
|
632
|
+
gap: 0.75rem;
|
|
750
633
|
}
|
|
751
634
|
|
|
752
635
|
@media (max-width: 1200px) {
|
|
@@ -767,74 +650,69 @@ pg_stat_statements.track = all</pre>
|
|
|
767
650
|
}
|
|
768
651
|
}
|
|
769
652
|
|
|
770
|
-
/* Metric Card
|
|
653
|
+
/* Metric Card — a plain bordered tile. Status is carried by the dot, the
|
|
654
|
+
value colour and the sparkline, never by a coloured edge: "healthy" is
|
|
655
|
+
the common case and should read as quiet, not decorated. */
|
|
771
656
|
.live-metric-card {
|
|
772
657
|
background: var(--bg-tertiary);
|
|
773
658
|
border: 1px solid var(--border-color);
|
|
774
|
-
border-radius:
|
|
775
|
-
padding: 1rem;
|
|
659
|
+
border-radius: var(--radius);
|
|
660
|
+
padding: 0.875rem 1rem;
|
|
776
661
|
position: relative;
|
|
777
|
-
transition:
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
.live-metric-card:hover {
|
|
781
|
-
border-color: var(--accent-purple);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
.live-metric-card.status-ok {
|
|
785
|
-
border-left: 3px solid var(--accent-green);
|
|
662
|
+
transition: border-color 0.12s ease;
|
|
786
663
|
}
|
|
787
664
|
|
|
788
665
|
.live-metric-card.status-warning {
|
|
789
|
-
border-
|
|
666
|
+
border-color: var(--amber-line);
|
|
790
667
|
}
|
|
791
668
|
|
|
792
669
|
.live-metric-card.status-critical {
|
|
793
|
-
border-
|
|
670
|
+
border-color: var(--rose-line);
|
|
794
671
|
}
|
|
795
672
|
|
|
796
673
|
.metric-header {
|
|
797
674
|
display: flex;
|
|
798
675
|
align-items: center;
|
|
799
676
|
gap: 0.5rem;
|
|
800
|
-
margin-bottom: 0.
|
|
677
|
+
margin-bottom: 0.625rem;
|
|
801
678
|
}
|
|
802
679
|
|
|
803
680
|
.metric-icon {
|
|
804
|
-
width:
|
|
805
|
-
height:
|
|
806
|
-
|
|
807
|
-
display: flex;
|
|
808
|
-
align-items: center;
|
|
809
|
-
justify-content: center;
|
|
810
|
-
font-size: 0.875rem;
|
|
681
|
+
width: 0.9375rem;
|
|
682
|
+
height: 0.9375rem;
|
|
683
|
+
color: var(--text-muted);
|
|
811
684
|
}
|
|
812
685
|
|
|
813
686
|
.metric-name {
|
|
814
|
-
font-size: 0.
|
|
687
|
+
font-size: 0.75rem;
|
|
815
688
|
font-weight: 500;
|
|
689
|
+
letter-spacing: 0.02em;
|
|
816
690
|
color: var(--text-secondary);
|
|
817
691
|
}
|
|
818
692
|
|
|
819
693
|
.metric-body {
|
|
820
694
|
display: flex;
|
|
821
695
|
flex-direction: column;
|
|
822
|
-
gap: 0.
|
|
696
|
+
gap: 0.1875rem;
|
|
823
697
|
}
|
|
824
698
|
|
|
825
699
|
.metric-value {
|
|
826
700
|
display: flex;
|
|
827
701
|
align-items: baseline;
|
|
828
|
-
gap: 0.
|
|
702
|
+
gap: 0.3rem;
|
|
829
703
|
}
|
|
830
704
|
|
|
831
705
|
.metric-value span:first-child {
|
|
832
|
-
font-size: 1.
|
|
833
|
-
font-weight:
|
|
706
|
+
font-size: 1.625rem;
|
|
707
|
+
font-weight: 600;
|
|
708
|
+
letter-spacing: -0.02em;
|
|
834
709
|
color: var(--text-primary);
|
|
835
|
-
line-height: 1;
|
|
710
|
+
line-height: 1.1;
|
|
836
711
|
}
|
|
837
712
|
|
|
713
|
+
.status-warning .metric-value span:first-child { color: var(--accent-amber); }
|
|
714
|
+
.status-critical .metric-value span:first-child { color: var(--accent-rose); }
|
|
715
|
+
|
|
838
716
|
.metric-unit {
|
|
839
717
|
font-size: 0.75rem;
|
|
840
718
|
color: var(--text-muted);
|
|
@@ -843,73 +721,274 @@ pg_stat_statements.track = all</pre>
|
|
|
843
721
|
.metric-detail {
|
|
844
722
|
font-size: 0.7rem;
|
|
845
723
|
color: var(--text-muted);
|
|
724
|
+
white-space: nowrap;
|
|
725
|
+
overflow: hidden;
|
|
726
|
+
text-overflow: ellipsis;
|
|
846
727
|
}
|
|
847
728
|
|
|
848
729
|
/* Sparkline */
|
|
849
730
|
.metric-sparkline {
|
|
850
|
-
height:
|
|
851
|
-
margin-top: 0.
|
|
731
|
+
height: 28px;
|
|
732
|
+
margin-top: 0.625rem;
|
|
733
|
+
color: var(--text-muted);
|
|
852
734
|
}
|
|
853
735
|
|
|
736
|
+
.status-warning .metric-sparkline { color: var(--accent-amber); }
|
|
737
|
+
.status-critical .metric-sparkline { color: var(--accent-rose); }
|
|
738
|
+
|
|
854
739
|
.metric-sparkline svg {
|
|
855
740
|
width: 100%;
|
|
856
741
|
height: 100%;
|
|
742
|
+
display: block;
|
|
857
743
|
}
|
|
858
744
|
|
|
859
745
|
/* Status indicator */
|
|
860
746
|
.metric-status {
|
|
861
747
|
position: absolute;
|
|
862
|
-
top: 0.
|
|
863
|
-
right: 0.
|
|
748
|
+
top: 0.75rem;
|
|
749
|
+
right: 0.75rem;
|
|
864
750
|
width: 6px;
|
|
865
751
|
height: 6px;
|
|
866
752
|
border-radius: 50%;
|
|
753
|
+
background: var(--border-strong);
|
|
867
754
|
}
|
|
868
755
|
|
|
869
|
-
.metric-status.ok { background: var(--accent-green); }
|
|
870
756
|
.metric-status.warning { background: var(--accent-amber); }
|
|
871
757
|
.metric-status.critical { background: var(--accent-rose); }
|
|
872
758
|
|
|
873
|
-
/*
|
|
874
|
-
.
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
759
|
+
/* Run Query Modal */
|
|
760
|
+
.modal-run-query {
|
|
761
|
+
max-width: 1100px;
|
|
762
|
+
width: 95%;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.explain-label {
|
|
766
|
+
display: block;
|
|
767
|
+
font-size: 0.6875rem;
|
|
768
|
+
font-weight: 600;
|
|
769
|
+
line-height: 1.4;
|
|
770
|
+
letter-spacing: 0.06em;
|
|
771
|
+
color: var(--text-muted);
|
|
772
|
+
text-transform: uppercase;
|
|
773
|
+
margin-bottom: 0.4rem;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.run-query-textarea {
|
|
777
|
+
width: 100%;
|
|
778
|
+
min-height: 180px;
|
|
779
|
+
padding: 0.875rem;
|
|
780
|
+
background: var(--bg-primary);
|
|
880
781
|
border: 1px solid var(--border-color);
|
|
881
|
-
border-radius:
|
|
782
|
+
border-radius: var(--radius-sm);
|
|
882
783
|
color: var(--text-primary);
|
|
883
|
-
font-
|
|
784
|
+
font-family: var(--font-mono);
|
|
785
|
+
font-size: 0.8125rem;
|
|
786
|
+
line-height: 1.6;
|
|
787
|
+
resize: vertical;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.run-query-textarea:focus-visible {
|
|
791
|
+
outline: 2px solid var(--accent-blue);
|
|
792
|
+
outline-offset: -1px;
|
|
793
|
+
border-color: var(--accent-blue);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.run-query-textarea::placeholder {
|
|
797
|
+
color: var(--text-muted);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.run-query-actions {
|
|
801
|
+
display: flex;
|
|
802
|
+
gap: 0.5rem;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/* Query Results Table (mirrors _show_styles.html.erb — index.html.erb has
|
|
806
|
+
its own <style> block and isn't loaded through that partial) */
|
|
807
|
+
.query-results-wrapper {
|
|
808
|
+
max-height: 400px;
|
|
809
|
+
overflow: auto;
|
|
810
|
+
border: 1px solid var(--border-color);
|
|
811
|
+
border-radius: var(--radius);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.query-results-table {
|
|
815
|
+
width: 100%;
|
|
816
|
+
border-collapse: collapse;
|
|
817
|
+
font-family: var(--font-mono);
|
|
818
|
+
font-size: 0.75rem;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.query-results-table th {
|
|
822
|
+
position: sticky;
|
|
823
|
+
top: 0;
|
|
824
|
+
padding: 0.75rem;
|
|
825
|
+
text-align: left;
|
|
826
|
+
background: var(--bg-tertiary);
|
|
827
|
+
color: var(--text-muted);
|
|
884
828
|
font-weight: 500;
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
829
|
+
text-transform: uppercase;
|
|
830
|
+
font-size: 0.65rem;
|
|
831
|
+
letter-spacing: 0.05em;
|
|
832
|
+
border-bottom: 1px solid var(--border-color);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.query-results-table td {
|
|
836
|
+
padding: 0.5rem 0.75rem;
|
|
837
|
+
border-bottom: 1px solid var(--border-color);
|
|
838
|
+
color: var(--text-secondary);
|
|
839
|
+
max-width: 300px;
|
|
840
|
+
overflow: hidden;
|
|
841
|
+
text-overflow: ellipsis;
|
|
842
|
+
white-space: nowrap;
|
|
890
843
|
}
|
|
891
844
|
|
|
892
|
-
.
|
|
893
|
-
|
|
894
|
-
|
|
845
|
+
.query-results-table tr:hover td {
|
|
846
|
+
background: var(--bg-tertiary);
|
|
847
|
+
color: var(--text-primary);
|
|
895
848
|
}
|
|
896
849
|
|
|
897
|
-
.
|
|
898
|
-
|
|
850
|
+
.query-results-info {
|
|
851
|
+
display: flex;
|
|
852
|
+
align-items: center;
|
|
853
|
+
justify-content: space-between;
|
|
854
|
+
padding: 0.75rem 1rem;
|
|
855
|
+
background: var(--bg-tertiary);
|
|
856
|
+
border-radius: var(--radius);
|
|
857
|
+
font-size: 0.8rem;
|
|
858
|
+
color: var(--text-secondary);
|
|
899
859
|
}
|
|
900
860
|
|
|
901
|
-
.
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
border-color: var(--accent-rose);
|
|
861
|
+
.query-results-info .count {
|
|
862
|
+
color: var(--accent-blue);
|
|
863
|
+
font-weight: 600;
|
|
905
864
|
}
|
|
906
865
|
|
|
907
|
-
.
|
|
908
|
-
|
|
866
|
+
.query-results-info .time {
|
|
867
|
+
color: var(--accent-green);
|
|
868
|
+
font-weight: 600;
|
|
909
869
|
}
|
|
910
870
|
</style>
|
|
911
871
|
|
|
912
872
|
<script>
|
|
873
|
+
const allowRawQueryExecution = <%= PgReports.config.allow_raw_query_execution.to_s.downcase %>;
|
|
874
|
+
|
|
875
|
+
function escapeHtmlAttr(text) {
|
|
876
|
+
return String(text)
|
|
877
|
+
.replace(/&/g, '&')
|
|
878
|
+
.replace(/"/g, '"')
|
|
879
|
+
.replace(/'/g, ''')
|
|
880
|
+
.replace(/</g, '<')
|
|
881
|
+
.replace(/>/g, '>');
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function showRunQueryModal() {
|
|
885
|
+
document.getElementById('run-query-modal').style.display = 'flex';
|
|
886
|
+
document.getElementById('run-query-input')?.focus();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function closeRunQueryModal() {
|
|
890
|
+
document.getElementById('run-query-modal').style.display = 'none';
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
document.getElementById('run-query-modal')?.addEventListener('click', function(e) {
|
|
894
|
+
if (e.target === this) closeRunQueryModal();
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
document.getElementById('run-query-input')?.addEventListener('keydown', function(e) {
|
|
898
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
899
|
+
e.preventDefault();
|
|
900
|
+
executeRunQuery();
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
function clearRunQuery() {
|
|
905
|
+
document.getElementById('run-query-input').value = '';
|
|
906
|
+
document.getElementById('run-query-content').innerHTML = '';
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async function executeRunQuery() {
|
|
910
|
+
const input = document.getElementById('run-query-input');
|
|
911
|
+
const loading = document.getElementById('run-query-loading');
|
|
912
|
+
const content = document.getElementById('run-query-content');
|
|
913
|
+
const query = input.value.trim();
|
|
914
|
+
|
|
915
|
+
if (!query) {
|
|
916
|
+
showToast(PG_REPORTS_I18N.errors.query_required, 'error');
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (!allowRawQueryExecution) {
|
|
921
|
+
showToast(PG_REPORTS_I18N.errors.execute_disabled_toast, 'error');
|
|
922
|
+
content.innerHTML = pgReportsDisabledNotice(PG_REPORTS_I18N.modals.query_execution_disabled_title);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
loading.style.display = 'flex';
|
|
927
|
+
content.innerHTML = '';
|
|
928
|
+
|
|
929
|
+
try {
|
|
930
|
+
const response = await fetch(`${pgReportsRoot}/run_query`, {
|
|
931
|
+
method: 'POST',
|
|
932
|
+
headers: {
|
|
933
|
+
'Content-Type': 'application/json',
|
|
934
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
935
|
+
},
|
|
936
|
+
body: JSON.stringify({ query: query })
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
const data = await response.json();
|
|
940
|
+
loading.style.display = 'none';
|
|
941
|
+
|
|
942
|
+
if (data.success) {
|
|
943
|
+
let html = '';
|
|
944
|
+
|
|
945
|
+
html += `<div class="query-results-info">
|
|
946
|
+
<span>${PG_REPORTS_I18N.results.rows_label} <span class="count">${data.count}</span></span>
|
|
947
|
+
<span>${PG_REPORTS_I18N.results.execution_time_label} <span class="time">${data.execution_time} ms</span></span>
|
|
948
|
+
</div>`;
|
|
949
|
+
|
|
950
|
+
if (data.rows && data.rows.length > 0) {
|
|
951
|
+
html += '<div class="query-results-wrapper">';
|
|
952
|
+
html += '<table class="query-results-table">';
|
|
953
|
+
|
|
954
|
+
html += '<thead><tr>';
|
|
955
|
+
data.columns.forEach(col => {
|
|
956
|
+
html += `<th>${escapeHtml(col)}</th>`;
|
|
957
|
+
});
|
|
958
|
+
html += '</tr></thead>';
|
|
959
|
+
|
|
960
|
+
html += '<tbody>';
|
|
961
|
+
data.rows.forEach(row => {
|
|
962
|
+
html += '<tr>';
|
|
963
|
+
data.columns.forEach(col => {
|
|
964
|
+
const value = row[col];
|
|
965
|
+
const displayValue = value === null ? PG_REPORTS_I18N.results.null_placeholder : String(value);
|
|
966
|
+
html += `<td title="${escapeHtmlAttr(displayValue)}">${escapeHtml(displayValue)}</td>`;
|
|
967
|
+
});
|
|
968
|
+
html += '</tr>';
|
|
969
|
+
});
|
|
970
|
+
html += '</tbody>';
|
|
971
|
+
|
|
972
|
+
html += '</table>';
|
|
973
|
+
html += '</div>';
|
|
974
|
+
|
|
975
|
+
if (data.truncated) {
|
|
976
|
+
html += `<p style="margin-top: 0.5rem; color: var(--text-muted); font-size: 0.75rem;">${pgReportsFormat(PG_REPORTS_I18N.results.showing_first_of_total, { count: data.rows.length, total: data.total_count })}</p>`;
|
|
977
|
+
}
|
|
978
|
+
} else {
|
|
979
|
+
html += '<p style="margin-top: 1rem; color: var(--text-muted);">' + PG_REPORTS_I18N.results.no_rows_returned + '</p>';
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
content.innerHTML = html;
|
|
983
|
+
} else {
|
|
984
|
+
content.innerHTML = `<div class="error-message">${escapeHtml(data.error || PG_REPORTS_I18N.errors.run_query_failed)}</div>`;
|
|
985
|
+
}
|
|
986
|
+
} catch (error) {
|
|
987
|
+
loading.style.display = 'none';
|
|
988
|
+
content.innerHTML = `<div class="error-message">${PG_REPORTS_I18N.errors.network_error_prefix} ${escapeHtml(error.message)}</div>`;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
913
992
|
function showPgStatInfo() {
|
|
914
993
|
document.getElementById('pg-stat-modal').style.display = 'flex';
|
|
915
994
|
}
|
|
@@ -1136,14 +1215,12 @@ pg_stat_statements.track = all</pre>
|
|
|
1136
1215
|
function updateToggleUI() {
|
|
1137
1216
|
const icon = document.getElementById('toggle-icon');
|
|
1138
1217
|
const indicator = document.querySelector('.live-indicator');
|
|
1218
|
+
const use = icon && icon.querySelector('use');
|
|
1139
1219
|
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1142
|
-
indicator.classList.remove('paused');
|
|
1143
|
-
} else {
|
|
1144
|
-
icon.textContent = '▶';
|
|
1145
|
-
indicator.classList.add('paused');
|
|
1220
|
+
if (use) {
|
|
1221
|
+
use.setAttribute('href', liveMonitoringEnabled ? '#i-pause' : '#i-play');
|
|
1146
1222
|
}
|
|
1223
|
+
indicator.classList.toggle('paused', !liveMonitoringEnabled);
|
|
1147
1224
|
}
|
|
1148
1225
|
|
|
1149
1226
|
async function fetchLiveMetrics() {
|
|
@@ -1216,18 +1293,18 @@ pg_stat_statements.track = all</pre>
|
|
|
1216
1293
|
addToHistory('longrunning', metrics.long_running_count);
|
|
1217
1294
|
addToHistory('blocked', metrics.blocked_count);
|
|
1218
1295
|
|
|
1219
|
-
renderSparkline('connections', metricsHistory.connections, 'var(--accent-blue)');
|
|
1220
|
-
renderSparkline('tps', metricsHistory.tps, 'var(--accent-green)');
|
|
1221
|
-
renderSparkline('cache', metricsHistory.cache, 'var(--accent-purple)');
|
|
1222
|
-
renderSparkline('longrunning', metricsHistory.longrunning, 'var(--accent-amber)');
|
|
1223
|
-
renderSparkline('blocked', metricsHistory.blocked, 'var(--accent-rose)');
|
|
1224
|
-
|
|
1225
1296
|
updateStatus('connections', metrics.connections.percent, 'higher');
|
|
1226
1297
|
updateStatus('tps', tps, null);
|
|
1227
1298
|
updateStatus('cache', metrics.cache_hit_ratio, 'lower');
|
|
1228
1299
|
updateStatus('longrunning', metrics.long_running_count, 'higher');
|
|
1229
1300
|
updateStatus('blocked', metrics.blocked_count, 'higher');
|
|
1230
1301
|
|
|
1302
|
+
renderSparkline('connections', metricsHistory.connections);
|
|
1303
|
+
renderSparkline('tps', metricsHistory.tps);
|
|
1304
|
+
renderSparkline('cache', metricsHistory.cache);
|
|
1305
|
+
renderSparkline('longrunning', metricsHistory.longrunning);
|
|
1306
|
+
renderSparkline('blocked', metricsHistory.blocked);
|
|
1307
|
+
|
|
1231
1308
|
previousMetrics = metrics;
|
|
1232
1309
|
}
|
|
1233
1310
|
|
|
@@ -1244,7 +1321,7 @@ pg_stat_statements.track = all</pre>
|
|
|
1244
1321
|
return num.toString();
|
|
1245
1322
|
}
|
|
1246
1323
|
|
|
1247
|
-
function renderSparkline(metric, data
|
|
1324
|
+
function renderSparkline(metric, data) {
|
|
1248
1325
|
const svg = document.getElementById(`sparkline-${metric}`);
|
|
1249
1326
|
if (!svg || data.length < 2) return;
|
|
1250
1327
|
|
|
@@ -1272,15 +1349,16 @@ pg_stat_statements.track = all</pre>
|
|
|
1272
1349
|
|
|
1273
1350
|
const area = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1274
1351
|
area.setAttribute('d', areaD);
|
|
1275
|
-
area.setAttribute('fill',
|
|
1276
|
-
area.setAttribute('opacity', '0.
|
|
1352
|
+
area.setAttribute('fill', 'currentColor');
|
|
1353
|
+
area.setAttribute('opacity', '0.12');
|
|
1277
1354
|
svg.appendChild(area);
|
|
1278
1355
|
|
|
1279
1356
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1280
1357
|
path.setAttribute('d', pathD);
|
|
1281
1358
|
path.setAttribute('fill', 'none');
|
|
1282
|
-
path.setAttribute('stroke',
|
|
1283
|
-
path.setAttribute('stroke-width', '1.
|
|
1359
|
+
path.setAttribute('stroke', 'currentColor');
|
|
1360
|
+
path.setAttribute('stroke-width', '1.25');
|
|
1361
|
+
path.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
1284
1362
|
path.setAttribute('stroke-linecap', 'round');
|
|
1285
1363
|
path.setAttribute('stroke-linejoin', 'round');
|
|
1286
1364
|
svg.appendChild(path);
|
|
@@ -1292,24 +1370,22 @@ pg_stat_statements.track = all</pre>
|
|
|
1292
1370
|
if (!card || !statusDot) return;
|
|
1293
1371
|
|
|
1294
1372
|
const thresholds = LIVE_CONFIG.thresholds[metric];
|
|
1295
|
-
if (!thresholds.warning && !thresholds.critical) {
|
|
1296
|
-
card.className = 'live-metric-card status-ok';
|
|
1297
|
-
statusDot.className = 'metric-status ok';
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
1373
|
let status = 'ok';
|
|
1302
1374
|
|
|
1303
|
-
if (
|
|
1304
|
-
if (
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
if (
|
|
1308
|
-
|
|
1375
|
+
if (thresholds.warning || thresholds.critical) {
|
|
1376
|
+
if (badDirection === 'higher') {
|
|
1377
|
+
if (value >= thresholds.critical) status = 'critical';
|
|
1378
|
+
else if (value >= thresholds.warning) status = 'warning';
|
|
1379
|
+
} else if (badDirection === 'lower') {
|
|
1380
|
+
if (value <= thresholds.critical) status = 'critical';
|
|
1381
|
+
else if (value <= thresholds.warning) status = 'warning';
|
|
1382
|
+
}
|
|
1309
1383
|
}
|
|
1310
1384
|
|
|
1311
|
-
card.
|
|
1312
|
-
|
|
1385
|
+
card.classList.remove('status-ok', 'status-warning', 'status-critical');
|
|
1386
|
+
card.classList.add(`status-${status}`);
|
|
1387
|
+
statusDot.classList.remove('ok', 'warning', 'critical');
|
|
1388
|
+
statusDot.classList.add(status);
|
|
1313
1389
|
}
|
|
1314
1390
|
|
|
1315
1391
|
document.addEventListener('DOMContentLoaded', initLiveMonitoring);
|
|
@@ -1495,6 +1571,8 @@ pg_stat_statements.track = all</pre>
|
|
|
1495
1571
|
const response = await fetch(`${pgReportsRoot}/query_monitor/status`);
|
|
1496
1572
|
const data = await response.json();
|
|
1497
1573
|
|
|
1574
|
+
queryHistoryAvailable = data.history_available === true;
|
|
1575
|
+
|
|
1498
1576
|
if (data.success && data.enabled) {
|
|
1499
1577
|
queryMonitorEnabled = true;
|
|
1500
1578
|
currentSessionId = data.session_id;
|
|
@@ -1513,6 +1591,17 @@ pg_stat_statements.track = all</pre>
|
|
|
1513
1591
|
}
|
|
1514
1592
|
}
|
|
1515
1593
|
|
|
1594
|
+
// Set from /query_monitor/status; null until the first check answers.
|
|
1595
|
+
let queryHistoryAvailable = null;
|
|
1596
|
+
|
|
1597
|
+
function applyHistoryAvailability(button) {
|
|
1598
|
+
const btn = button || document.getElementById('load-history-btn');
|
|
1599
|
+
if (!btn || queryHistoryAvailable === null) return;
|
|
1600
|
+
|
|
1601
|
+
btn.disabled = !queryHistoryAvailable;
|
|
1602
|
+
btn.title = queryHistoryAvailable ? '' : PG_REPORTS_I18N.actions.load_history_unavailable_title;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1516
1605
|
async function loadQueryHistory(button) {
|
|
1517
1606
|
button.disabled = true;
|
|
1518
1607
|
button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.loading;
|
|
@@ -1533,8 +1622,9 @@ pg_stat_statements.track = all</pre>
|
|
|
1533
1622
|
console.error('Failed to load query history:', error);
|
|
1534
1623
|
showToast(PG_REPORTS_I18N.errors.load_history_failed + ' ' + error.message, 'error');
|
|
1535
1624
|
} finally {
|
|
1536
|
-
button.disabled = false;
|
|
1537
1625
|
button.innerHTML = PG_REPORTS_I18N.actions.load_history;
|
|
1626
|
+
button.disabled = false;
|
|
1627
|
+
applyHistoryAvailability(button);
|
|
1538
1628
|
}
|
|
1539
1629
|
}
|
|
1540
1630
|
|
|
@@ -1635,6 +1725,7 @@ pg_stat_statements.track = all</pre>
|
|
|
1635
1725
|
startBtn.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
|
|
1636
1726
|
stopBtn.style.display = 'none';
|
|
1637
1727
|
loadHistoryBtn.style.display = 'inline-block'; // Show when monitoring stopped
|
|
1728
|
+
applyHistoryAvailability(loadHistoryBtn);
|
|
1638
1729
|
sessionBadge.style.display = 'none';
|
|
1639
1730
|
|
|
1640
1731
|
// Keep results visible after stopping, only hide download if no queries
|