pg_reports 0.8.2 → 0.9.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 +4 -4
- data/CHANGELOG.md +48 -0
- data/app/controllers/pg_reports/dashboard_controller.rb +104 -6
- data/app/views/layouts/pg_reports/application.html.erb +712 -214
- data/app/views/pg_reports/dashboard/_database_selector.html.erb +2 -1
- data/app/views/pg_reports/dashboard/_settings_modal.html.erb +55 -0
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +11 -49
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +101 -90
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +392 -476
- data/app/views/pg_reports/dashboard/index.html.erb +217 -493
- data/app/views/pg_reports/dashboard/show.html.erb +13 -11
- data/config/brakeman.ignore +24 -1
- data/config/locales/en.yml +58 -50
- data/config/locales/ru.yml +58 -50
- data/config/locales/uk.yml +58 -50
- data/config/routes.rb +1 -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/query_monitor.rb +15 -3
- data/lib/pg_reports/version.rb +1 -1
- metadata +2 -1
|
@@ -12,26 +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">
|
|
26
|
-
<button class="btn btn-
|
|
23
|
+
<button class="btn btn-primary" onclick="showRunQueryModal()" id="run-query-open-btn">
|
|
27
24
|
<%= t("pg_reports.ui.actions.run_query") %>
|
|
28
25
|
</button>
|
|
29
26
|
<% if @pg_stat_status[:ready] %>
|
|
30
|
-
<button class="btn btn-
|
|
27
|
+
<button class="btn btn-muted" onclick="showResetConfirmModal()" id="reset-btn">
|
|
31
28
|
<%= t("pg_reports.ui.actions.reset_statistics") %>
|
|
32
29
|
</button>
|
|
33
30
|
<% end %>
|
|
34
|
-
<button class="btn-
|
|
31
|
+
<button type="button" class="btn-icon" onclick="showSettingsModal()" title="<%= t("pg_reports.ui.actions.settings_button_title") %>" aria-label="<%= t("pg_reports.ui.actions.settings_button_title") %>">
|
|
32
|
+
<svg class="icon icon-lg"><use href="#i-settings"></use></svg>
|
|
33
|
+
</button>
|
|
35
34
|
|
|
36
35
|
<% if !@pg_stat_status[:connected] %>
|
|
37
36
|
<div class="header-badge" id="pg-stat-badge">
|
|
@@ -48,14 +47,14 @@
|
|
|
48
47
|
onclick="showPgStatInfo()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
|
|
49
48
|
<span class="badge-dot warning"></span>
|
|
50
49
|
<span><%= t("pg_reports.ui.status.not_preloaded") %></span>
|
|
51
|
-
<
|
|
50
|
+
<svg class="icon badge-info-icon" aria-hidden="true"><use href="#i-info"></use></svg>
|
|
52
51
|
</button>
|
|
53
52
|
<% else %>
|
|
54
53
|
<button type="button" class="header-badge header-badge-clickable" id="pg-stat-badge"
|
|
55
54
|
onclick="showCreateExtensionModal()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
|
|
56
55
|
<span class="badge-dot warning"></span>
|
|
57
56
|
<span><%= t("pg_reports.ui.status.extension_missing") %></span>
|
|
58
|
-
<
|
|
57
|
+
<svg class="icon badge-info-icon" aria-hidden="true"><use href="#i-info"></use></svg>
|
|
59
58
|
</button>
|
|
60
59
|
<% end %>
|
|
61
60
|
</div>
|
|
@@ -128,47 +127,7 @@ pg_stat_statements.track = all</pre>
|
|
|
128
127
|
</div>
|
|
129
128
|
|
|
130
129
|
<!-- IDE Settings Modal -->
|
|
131
|
-
|
|
132
|
-
<div class="modal-content modal-small">
|
|
133
|
-
<div class="modal-header">
|
|
134
|
-
<h3><%= t("pg_reports.ui.modals.ide_settings_title") %></h3>
|
|
135
|
-
<button class="modal-close" onclick="closeIdeSettingsModal()">×</button>
|
|
136
|
-
</div>
|
|
137
|
-
<div class="modal-body">
|
|
138
|
-
<p class="settings-label"><%= t("pg_reports.ui.settings.default_ide_label") %></p>
|
|
139
|
-
<div class="ide-options">
|
|
140
|
-
<label class="ide-option">
|
|
141
|
-
<input type="radio" name="default-ide" value="" onchange="setDefaultIde('')">
|
|
142
|
-
<span><%= t("pg_reports.ui.settings.ide_show_menu") %></span>
|
|
143
|
-
</label>
|
|
144
|
-
<label class="ide-option">
|
|
145
|
-
<input type="radio" name="default-ide" value="vscode-wsl" onchange="setDefaultIde('vscode-wsl')">
|
|
146
|
-
<span><%= t("pg_reports.ui.settings.ide_vscode_wsl") %></span>
|
|
147
|
-
</label>
|
|
148
|
-
<label class="ide-option">
|
|
149
|
-
<input type="radio" name="default-ide" value="vscode" onchange="setDefaultIde('vscode')">
|
|
150
|
-
<span><%= t("pg_reports.ui.settings.ide_vscode") %></span>
|
|
151
|
-
</label>
|
|
152
|
-
<label class="ide-option">
|
|
153
|
-
<input type="radio" name="default-ide" value="rubymine" onchange="setDefaultIde('rubymine')">
|
|
154
|
-
<span><%= t("pg_reports.ui.settings.ide_rubymine") %></span>
|
|
155
|
-
</label>
|
|
156
|
-
<label class="ide-option">
|
|
157
|
-
<input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
|
|
158
|
-
<span><%= t("pg_reports.ui.settings.ide_intellij") %></span>
|
|
159
|
-
</label>
|
|
160
|
-
<label class="ide-option">
|
|
161
|
-
<input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
|
|
162
|
-
<span><%= t("pg_reports.ui.settings.ide_cursor_wsl") %></span>
|
|
163
|
-
</label>
|
|
164
|
-
<label class="ide-option">
|
|
165
|
-
<input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
|
|
166
|
-
<span><%= t("pg_reports.ui.settings.ide_cursor") %></span>
|
|
167
|
-
</label>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
130
|
+
<%= render "settings_modal" %>
|
|
172
131
|
|
|
173
132
|
<!-- Run Query Modal -->
|
|
174
133
|
<div id="run-query-modal" class="modal" style="display: none;">
|
|
@@ -191,15 +150,13 @@ pg_stat_statements.track = all</pre>
|
|
|
191
150
|
<div id="run-query-content"></div>
|
|
192
151
|
<% else %>
|
|
193
152
|
<div class="error-message">
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<p style="margin-top: 0.75rem;"><%= t("pg_reports.ui.modals.query_execution_disabled_env_note") %></p>
|
|
202
|
-
<code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION=true</code>
|
|
153
|
+
<p class="error-title"><%= t("pg_reports.ui.modals.query_execution_disabled_title") %></p>
|
|
154
|
+
<p><%= t("pg_reports.ui.modals.query_execution_disabled_intro") %></p>
|
|
155
|
+
<pre class="code-block">PgReports.configure do |config|
|
|
156
|
+
config.allow_raw_query_execution = true
|
|
157
|
+
end</pre>
|
|
158
|
+
<p><%= t("pg_reports.ui.modals.query_execution_disabled_env_note") %></p>
|
|
159
|
+
<pre class="code-block">PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION=true</pre>
|
|
203
160
|
</div>
|
|
204
161
|
<% end %>
|
|
205
162
|
</div>
|
|
@@ -217,8 +174,8 @@ end
|
|
|
217
174
|
</div>
|
|
218
175
|
<div class="live-monitoring-controls">
|
|
219
176
|
<span class="live-monitoring-interval"><%= t("pg_reports.ui.monitoring.update_interval") %></span>
|
|
220
|
-
<button class="btn-
|
|
221
|
-
<
|
|
177
|
+
<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") %>">
|
|
178
|
+
<svg class="icon" id="toggle-icon"><use href="#i-pause"></use></svg>
|
|
222
179
|
</button>
|
|
223
180
|
</div>
|
|
224
181
|
</div>
|
|
@@ -227,7 +184,7 @@ end
|
|
|
227
184
|
<!-- Connections -->
|
|
228
185
|
<div class="live-metric-card" data-metric="connections">
|
|
229
186
|
<div class="metric-header">
|
|
230
|
-
<
|
|
187
|
+
<svg class="icon metric-icon"><use href="#i-link"></use></svg>
|
|
231
188
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.connections_label") %></span>
|
|
232
189
|
</div>
|
|
233
190
|
<div class="metric-body">
|
|
@@ -242,13 +199,12 @@ end
|
|
|
242
199
|
<svg id="sparkline-connections" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
243
200
|
</div>
|
|
244
201
|
</div>
|
|
245
|
-
<div class="metric-status" id="status-connections"></div>
|
|
246
202
|
</div>
|
|
247
203
|
|
|
248
204
|
<!-- TPS -->
|
|
249
205
|
<div class="live-metric-card" data-metric="tps">
|
|
250
206
|
<div class="metric-header">
|
|
251
|
-
<
|
|
207
|
+
<svg class="icon metric-icon"><use href="#i-activity"></use></svg>
|
|
252
208
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.tps_label") %></span>
|
|
253
209
|
</div>
|
|
254
210
|
<div class="metric-body">
|
|
@@ -263,13 +219,12 @@ end
|
|
|
263
219
|
<svg id="sparkline-tps" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
264
220
|
</div>
|
|
265
221
|
</div>
|
|
266
|
-
<div class="metric-status" id="status-tps"></div>
|
|
267
222
|
</div>
|
|
268
223
|
|
|
269
224
|
<!-- Cache Hit Ratio -->
|
|
270
225
|
<div class="live-metric-card" data-metric="cache">
|
|
271
226
|
<div class="metric-header">
|
|
272
|
-
<
|
|
227
|
+
<svg class="icon metric-icon"><use href="#i-database"></use></svg>
|
|
273
228
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.cache_hit_label") %></span>
|
|
274
229
|
</div>
|
|
275
230
|
<div class="metric-body">
|
|
@@ -282,13 +237,12 @@ end
|
|
|
282
237
|
<svg id="sparkline-cache" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
283
238
|
</div>
|
|
284
239
|
</div>
|
|
285
|
-
<div class="metric-status" id="status-cache"></div>
|
|
286
240
|
</div>
|
|
287
241
|
|
|
288
242
|
<!-- Long Running Queries -->
|
|
289
243
|
<div class="live-metric-card" data-metric="longrunning">
|
|
290
244
|
<div class="metric-header">
|
|
291
|
-
<
|
|
245
|
+
<svg class="icon metric-icon"><use href="#i-clock"></use></svg>
|
|
292
246
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.long_queries_label") %></span>
|
|
293
247
|
</div>
|
|
294
248
|
<div class="metric-body">
|
|
@@ -301,13 +255,12 @@ end
|
|
|
301
255
|
<svg id="sparkline-longrunning" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
302
256
|
</div>
|
|
303
257
|
</div>
|
|
304
|
-
<div class="metric-status" id="status-longrunning"></div>
|
|
305
258
|
</div>
|
|
306
259
|
|
|
307
260
|
<!-- Blocked Queries -->
|
|
308
261
|
<div class="live-metric-card" data-metric="blocked">
|
|
309
262
|
<div class="metric-header">
|
|
310
|
-
<
|
|
263
|
+
<svg class="icon metric-icon"><use href="#i-lock"></use></svg>
|
|
311
264
|
<span class="metric-name"><%= t("pg_reports.ui.metrics.blocked_label") %></span>
|
|
312
265
|
</div>
|
|
313
266
|
<div class="metric-body">
|
|
@@ -320,7 +273,6 @@ end
|
|
|
320
273
|
<svg id="sparkline-blocked" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
321
274
|
</div>
|
|
322
275
|
</div>
|
|
323
|
-
<div class="metric-status" id="status-blocked"></div>
|
|
324
276
|
</div>
|
|
325
277
|
</div>
|
|
326
278
|
</div>
|
|
@@ -343,17 +295,17 @@ end
|
|
|
343
295
|
</span>
|
|
344
296
|
</div>
|
|
345
297
|
<div class="query-monitoring-controls">
|
|
346
|
-
<button class="btn btn-
|
|
298
|
+
<button class="btn btn-primary" onclick="startQueryMonitoring(this)" id="start-monitor-btn">
|
|
347
299
|
<%= t("pg_reports.ui.actions.start_monitoring") %>
|
|
348
300
|
</button>
|
|
349
|
-
<button class="btn btn-
|
|
301
|
+
<button class="btn btn-danger" onclick="stopQueryMonitoring(this)" id="stop-monitor-btn" style="display: none;">
|
|
350
302
|
<%= t("pg_reports.ui.actions.stop_monitoring") %>
|
|
351
303
|
</button>
|
|
352
|
-
<button class="btn btn-
|
|
304
|
+
<button class="btn btn-secondary" onclick="loadQueryHistory(this)" id="load-history-btn" disabled>
|
|
353
305
|
<%= t("pg_reports.ui.actions.load_history") %>
|
|
354
306
|
</button>
|
|
355
307
|
<div class="download-dropdown" id="monitor-download-dropdown" style="display: none;">
|
|
356
|
-
<button class="btn btn-
|
|
308
|
+
<button class="btn btn-secondary" onclick="toggleMonitorDownloadMenu(event)">
|
|
357
309
|
<%= t("pg_reports.ui.actions.download") %>
|
|
358
310
|
</button>
|
|
359
311
|
<div class="download-menu" id="monitor-download-menu" style="display: none;">
|
|
@@ -376,34 +328,33 @@ end
|
|
|
376
328
|
</div>
|
|
377
329
|
<% end %>
|
|
378
330
|
|
|
331
|
+
<%
|
|
332
|
+
# Sprite ids for the category glyphs defined in the layout. The registry's own
|
|
333
|
+
# :icon/:color keys stay untouched — they are part of its public shape.
|
|
334
|
+
category_icons = {
|
|
335
|
+
queries: "queries", indexes: "indexes", tables: "tables",
|
|
336
|
+
connections: "connections", system: "system", schema_analysis: "schema"
|
|
337
|
+
}
|
|
338
|
+
%>
|
|
379
339
|
<div class="categories-grid">
|
|
380
340
|
<% @categories.each do |category_key, category| %>
|
|
381
|
-
<%
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
is_disabled = requires_pg_stat || target_disabled_reason.present?
|
|
385
|
-
%>
|
|
386
|
-
<div class="category-card<%= ' disabled' if is_disabled %>">
|
|
387
|
-
<% if requires_pg_stat %>
|
|
341
|
+
<% disabled_reason = category_disabled_reason(category_key) %>
|
|
342
|
+
<div class="category-card<%= ' disabled' if disabled_reason %>">
|
|
343
|
+
<% if disabled_reason %>
|
|
388
344
|
<div class="category-warning">
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
<% elsif target_disabled_reason %>
|
|
392
|
-
<div class="category-warning">
|
|
393
|
-
<%= target_disabled_reason %>
|
|
345
|
+
<svg class="icon" aria-hidden="true"><use href="#i-lock"></use></svg>
|
|
346
|
+
<span><%= disabled_reason %></span>
|
|
394
347
|
</div>
|
|
395
348
|
<% end %>
|
|
396
349
|
<div class="category-header">
|
|
397
|
-
<
|
|
398
|
-
<%= category[:icon] %>
|
|
399
|
-
</div>
|
|
350
|
+
<svg class="icon category-icon" aria-hidden="true"><use href="#i-<%= category_icons.fetch(category_key, "tables") %>"></use></svg>
|
|
400
351
|
<span class="category-title"><%= category[:name] %></span>
|
|
401
352
|
<span class="category-count"><%= category[:reports].size %> <%= t("pg_reports.ui.categories.reports_count_suffix") %></span>
|
|
402
353
|
</div>
|
|
403
354
|
|
|
404
355
|
<div class="reports-list">
|
|
405
356
|
<% category[:reports].each do |report_key, report| %>
|
|
406
|
-
<% if
|
|
357
|
+
<% if disabled_reason %>
|
|
407
358
|
<div class="report-link disabled">
|
|
408
359
|
<div class="report-link-info">
|
|
409
360
|
<span class="report-link-name">
|
|
@@ -412,7 +363,7 @@ end
|
|
|
412
363
|
</span>
|
|
413
364
|
<span class="report-link-desc"><%= report[:description] %></span>
|
|
414
365
|
</div>
|
|
415
|
-
<
|
|
366
|
+
<svg class="icon lock" aria-hidden="true"><use href="#i-lock"></use></svg>
|
|
416
367
|
</div>
|
|
417
368
|
<% else %>
|
|
418
369
|
<%= link_to report_path(category: category_key, report: report_key), class: "report-link" do %>
|
|
@@ -423,7 +374,7 @@ end
|
|
|
423
374
|
</span>
|
|
424
375
|
<span class="report-link-desc"><%= report[:description] %></span>
|
|
425
376
|
</div>
|
|
426
|
-
<
|
|
377
|
+
<svg class="icon arrow" aria-hidden="true"><use href="#i-chevron-right"></use></svg>
|
|
427
378
|
<% end %>
|
|
428
379
|
<% end %>
|
|
429
380
|
<% end %>
|
|
@@ -456,7 +407,7 @@ end
|
|
|
456
407
|
text-decoration: none;
|
|
457
408
|
}
|
|
458
409
|
.dashboard-footer a:hover {
|
|
459
|
-
color: var(--
|
|
410
|
+
color: var(--text-primary);
|
|
460
411
|
text-decoration: underline;
|
|
461
412
|
}
|
|
462
413
|
.dashboard-footer .footer-sep {
|
|
@@ -469,104 +420,45 @@ end
|
|
|
469
420
|
gap: 0.5rem;
|
|
470
421
|
}
|
|
471
422
|
|
|
472
|
-
.btn.btn-small {
|
|
473
|
-
padding: 0.5rem 1rem;
|
|
474
|
-
font-size: 0.8rem;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/* Keep the header action button the same height as the status badge and
|
|
478
|
-
the settings button (all 2.125rem); inline-flex centers the label so no
|
|
479
|
-
vertical padding is needed. */
|
|
480
|
-
.header-actions .btn {
|
|
481
|
-
height: 2.125rem;
|
|
482
|
-
padding: 0 1rem;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
.btn-info {
|
|
486
|
-
width: 2.125rem;
|
|
487
|
-
height: 2.125rem;
|
|
488
|
-
padding: 0;
|
|
489
|
-
display: inline-flex;
|
|
490
|
-
align-items: center;
|
|
491
|
-
justify-content: center;
|
|
492
|
-
background: var(--bg-tertiary);
|
|
493
|
-
color: var(--text-muted);
|
|
494
|
-
border: 1px solid var(--border-color);
|
|
495
|
-
border-radius: 4px;
|
|
496
|
-
font-size: 0.9rem;
|
|
497
|
-
font-weight: 600;
|
|
498
|
-
cursor: pointer;
|
|
499
|
-
transition: all 0.15s;
|
|
500
|
-
}
|
|
501
|
-
.btn-info:hover {
|
|
502
|
-
background: var(--accent-purple);
|
|
503
|
-
color: white;
|
|
504
|
-
border-color: var(--accent-purple);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
.btn.btn-danger {
|
|
508
|
-
background: var(--accent-rose);
|
|
509
|
-
border-color: var(--accent-rose);
|
|
510
|
-
}
|
|
511
|
-
.btn.btn-danger:hover {
|
|
512
|
-
background: #c45a6e;
|
|
513
|
-
border-color: #c45a6e;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
.btn.btn-secondary {
|
|
517
|
-
background: var(--bg-tertiary);
|
|
518
|
-
border-color: var(--border-color);
|
|
519
|
-
color: var(--text-secondary);
|
|
520
|
-
}
|
|
521
|
-
.btn.btn-secondary:hover {
|
|
522
|
-
background: var(--bg-card);
|
|
523
|
-
color: var(--text-primary);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
.btn.btn-muted {
|
|
527
|
-
background: var(--bg-tertiary);
|
|
528
|
-
border-color: var(--accent-rose);
|
|
529
|
-
color: var(--text-secondary);
|
|
530
|
-
}
|
|
531
|
-
.btn.btn-muted:hover {
|
|
532
|
-
background: var(--bg-card);
|
|
533
|
-
color: var(--text-primary);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
423
|
.badge-dot.error { background: var(--accent-rose); }
|
|
537
424
|
|
|
538
425
|
.category-card.disabled {
|
|
539
426
|
opacity: 0.7;
|
|
540
427
|
}
|
|
541
428
|
|
|
429
|
+
/* Why a category is unavailable. The icon carries the state; the reason
|
|
430
|
+
itself is body copy, not a wall of amber. */
|
|
542
431
|
.category-warning {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
432
|
+
display: flex;
|
|
433
|
+
align-items: flex-start;
|
|
434
|
+
gap: 0.45rem;
|
|
435
|
+
padding: 0.5rem 1rem;
|
|
436
|
+
margin: -1rem -1rem 1rem;
|
|
437
|
+
background: var(--bg-tertiary);
|
|
438
|
+
border-bottom: 1px solid var(--border-color);
|
|
439
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
440
|
+
color: var(--text-secondary);
|
|
441
|
+
font-size: 0.75rem;
|
|
442
|
+
line-height: 1.5;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.category-warning .icon {
|
|
446
|
+
width: 0.8125rem;
|
|
447
|
+
height: 0.8125rem;
|
|
448
|
+
margin-top: 0.25rem;
|
|
548
449
|
color: var(--accent-amber);
|
|
549
|
-
font-size: 0.8rem;
|
|
550
|
-
font-weight: 500;
|
|
551
|
-
text-align: center;
|
|
552
450
|
}
|
|
553
451
|
|
|
554
452
|
.report-link.disabled {
|
|
555
|
-
|
|
556
|
-
align-items: center;
|
|
557
|
-
justify-content: space-between;
|
|
558
|
-
padding: 0.75rem 1rem;
|
|
559
|
-
background: var(--bg-tertiary);
|
|
560
|
-
border: 1px solid transparent;
|
|
561
|
-
border-radius: 6px;
|
|
453
|
+
background: transparent;
|
|
562
454
|
color: var(--text-muted);
|
|
563
|
-
font-size: 0.9rem;
|
|
564
455
|
cursor: not-allowed;
|
|
565
456
|
}
|
|
566
457
|
|
|
567
458
|
.report-link .lock {
|
|
568
|
-
|
|
569
|
-
|
|
459
|
+
width: 0.8125rem;
|
|
460
|
+
height: 0.8125rem;
|
|
461
|
+
opacity: 0.6;
|
|
570
462
|
}
|
|
571
463
|
|
|
572
464
|
.report-link-info {
|
|
@@ -594,66 +486,8 @@ end
|
|
|
594
486
|
color: var(--text-secondary);
|
|
595
487
|
}
|
|
596
488
|
|
|
597
|
-
/* Modal */
|
|
598
|
-
.modal {
|
|
599
|
-
position: fixed;
|
|
600
|
-
inset: 0;
|
|
601
|
-
background: rgba(0, 0, 0, 0.7);
|
|
602
|
-
display: flex;
|
|
603
|
-
align-items: center;
|
|
604
|
-
justify-content: center;
|
|
605
|
-
z-index: 1000;
|
|
606
|
-
backdrop-filter: blur(4px);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
.modal-content {
|
|
610
|
-
background: var(--bg-card);
|
|
611
|
-
border: 1px solid var(--border-color);
|
|
612
|
-
border-radius: 6px;
|
|
613
|
-
max-width: 600px;
|
|
614
|
-
width: 90%;
|
|
615
|
-
max-height: 80vh;
|
|
616
|
-
overflow: auto;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
.modal-header {
|
|
620
|
-
display: flex;
|
|
621
|
-
align-items: center;
|
|
622
|
-
justify-content: space-between;
|
|
623
|
-
padding: 1.25rem 1.5rem;
|
|
624
|
-
border-bottom: 1px solid var(--border-color);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
.modal-header h3 {
|
|
628
|
-
font-size: 1.125rem;
|
|
629
|
-
font-weight: 600;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
.modal-close {
|
|
633
|
-
width: 32px;
|
|
634
|
-
height: 32px;
|
|
635
|
-
background: var(--bg-tertiary);
|
|
636
|
-
border: 1px solid var(--border-color);
|
|
637
|
-
border-radius: 8px;
|
|
638
|
-
color: var(--text-muted);
|
|
639
|
-
font-size: 1.25rem;
|
|
640
|
-
cursor: pointer;
|
|
641
|
-
transition: all 0.15s;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
.modal-close:hover {
|
|
645
|
-
background: var(--accent-rose);
|
|
646
|
-
border-color: var(--accent-rose);
|
|
647
|
-
color: white;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
.modal-body {
|
|
651
|
-
padding: 1.5rem;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
489
|
.modal-body p {
|
|
655
490
|
color: var(--text-secondary);
|
|
656
|
-
margin-bottom: 1rem;
|
|
657
491
|
}
|
|
658
492
|
|
|
659
493
|
.modal-body ol {
|
|
@@ -661,33 +495,14 @@ end
|
|
|
661
495
|
color: var(--text-secondary);
|
|
662
496
|
}
|
|
663
497
|
|
|
664
|
-
.modal-body li {
|
|
665
|
-
margin-bottom: 1.25rem;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
498
|
.modal-body strong {
|
|
669
499
|
color: var(--text-primary);
|
|
670
500
|
}
|
|
671
501
|
|
|
672
|
-
|
|
502
|
+
/* A step's label and its snippet are one unit — tighter than the gap
|
|
503
|
+
between steps. */
|
|
504
|
+
.modal-body li > strong + pre {
|
|
673
505
|
margin-top: 0.5rem;
|
|
674
|
-
padding: 1rem;
|
|
675
|
-
background: var(--bg-primary);
|
|
676
|
-
border: 1px solid var(--border-color);
|
|
677
|
-
border-radius: 8px;
|
|
678
|
-
font-family: 'JetBrains Mono', monospace;
|
|
679
|
-
font-size: 0.8rem;
|
|
680
|
-
color: var(--accent-green);
|
|
681
|
-
overflow-x: auto;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
.modal-small {
|
|
685
|
-
max-width: 420px;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
.modal-header-danger {
|
|
689
|
-
background: rgba(244, 63, 94, 0.1);
|
|
690
|
-
border-bottom-color: rgba(244, 63, 94, 0.2);
|
|
691
506
|
}
|
|
692
507
|
|
|
693
508
|
.modal-header-danger h3 {
|
|
@@ -695,58 +510,55 @@ end
|
|
|
695
510
|
}
|
|
696
511
|
|
|
697
512
|
.warning-text {
|
|
698
|
-
font-size:
|
|
513
|
+
font-size: 0.9375rem;
|
|
699
514
|
font-weight: 500;
|
|
700
|
-
color: var(--text-primary)
|
|
701
|
-
margin-bottom: 0.5rem !important;
|
|
515
|
+
color: var(--text-primary);
|
|
702
516
|
}
|
|
703
517
|
|
|
704
518
|
.warning-subtext {
|
|
705
|
-
font-size: 0.
|
|
706
|
-
color: var(--text-muted)
|
|
707
|
-
margin-bottom: 1.5rem !important;
|
|
519
|
+
font-size: 0.8125rem;
|
|
520
|
+
color: var(--text-muted);
|
|
708
521
|
}
|
|
709
522
|
|
|
710
523
|
.modal-actions {
|
|
711
524
|
display: flex;
|
|
712
|
-
gap: 0.
|
|
525
|
+
gap: 0.5rem;
|
|
713
526
|
justify-content: flex-end;
|
|
714
527
|
}
|
|
715
528
|
|
|
716
|
-
.modal-actions .btn {
|
|
717
|
-
padding: 0.625rem 1.25rem;
|
|
718
|
-
font-size: 0.875rem;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
529
|
/* Live Monitoring Panel */
|
|
722
530
|
.live-monitoring-panel {
|
|
723
|
-
margin-bottom:
|
|
531
|
+
margin-bottom: 1.5rem;
|
|
724
532
|
background: var(--bg-card);
|
|
725
533
|
border: 1px solid var(--border-color);
|
|
726
|
-
border-radius:
|
|
727
|
-
padding:
|
|
534
|
+
border-radius: var(--radius);
|
|
535
|
+
padding: 0.875rem 1rem;
|
|
728
536
|
}
|
|
729
537
|
|
|
730
538
|
.live-monitoring-header {
|
|
731
539
|
display: flex;
|
|
732
540
|
align-items: center;
|
|
733
541
|
justify-content: space-between;
|
|
734
|
-
|
|
735
|
-
|
|
542
|
+
gap: 1rem;
|
|
543
|
+
flex-wrap: wrap;
|
|
544
|
+
margin-bottom: 0.875rem;
|
|
545
|
+
padding-bottom: 0.75rem;
|
|
736
546
|
border-bottom: 1px solid var(--border-color);
|
|
737
547
|
}
|
|
738
548
|
|
|
739
549
|
.live-monitoring-title {
|
|
740
550
|
display: flex;
|
|
741
551
|
align-items: center;
|
|
742
|
-
gap: 0.
|
|
552
|
+
gap: 0.625rem;
|
|
743
553
|
font-weight: 600;
|
|
744
|
-
font-size:
|
|
554
|
+
font-size: 0.875rem;
|
|
555
|
+
min-width: 0;
|
|
745
556
|
}
|
|
746
557
|
|
|
747
558
|
.live-indicator {
|
|
748
|
-
width:
|
|
749
|
-
height:
|
|
559
|
+
width: 7px;
|
|
560
|
+
height: 7px;
|
|
561
|
+
flex: none;
|
|
750
562
|
border-radius: 50%;
|
|
751
563
|
background: var(--accent-green);
|
|
752
564
|
animation: pulse 2s infinite;
|
|
@@ -768,29 +580,11 @@ end
|
|
|
768
580
|
color: var(--text-muted);
|
|
769
581
|
}
|
|
770
582
|
|
|
771
|
-
.btn-toggle-live {
|
|
772
|
-
width: 32px;
|
|
773
|
-
height: 32px;
|
|
774
|
-
padding: 0;
|
|
775
|
-
background: var(--bg-tertiary);
|
|
776
|
-
border: 1px solid var(--border-color);
|
|
777
|
-
border-radius: 8px;
|
|
778
|
-
color: var(--text-muted);
|
|
779
|
-
font-size: 0.875rem;
|
|
780
|
-
cursor: pointer;
|
|
781
|
-
transition: all 0.15s;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
.btn-toggle-live:hover {
|
|
785
|
-
background: var(--bg-secondary);
|
|
786
|
-
color: var(--text-primary);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
583
|
/* Metrics Grid */
|
|
790
584
|
.live-metrics-grid {
|
|
791
585
|
display: grid;
|
|
792
586
|
grid-template-columns: repeat(5, 1fr);
|
|
793
|
-
gap:
|
|
587
|
+
gap: 0.75rem;
|
|
794
588
|
}
|
|
795
589
|
|
|
796
590
|
@media (max-width: 1200px) {
|
|
@@ -811,145 +605,112 @@ end
|
|
|
811
605
|
}
|
|
812
606
|
}
|
|
813
607
|
|
|
814
|
-
/* Metric Card
|
|
608
|
+
/* Metric Card — a plain bordered tile. Status is carried by the dot, the
|
|
609
|
+
value colour and the sparkline, never by a coloured edge: "healthy" is
|
|
610
|
+
the common case and should read as quiet, not decorated. */
|
|
815
611
|
.live-metric-card {
|
|
612
|
+
display: flex;
|
|
613
|
+
flex-direction: column;
|
|
816
614
|
background: var(--bg-tertiary);
|
|
817
615
|
border: 1px solid var(--border-color);
|
|
818
|
-
border-radius:
|
|
819
|
-
padding: 1rem;
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
.live-metric-card:hover {
|
|
825
|
-
border-color: var(--accent-purple);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
.live-metric-card.status-ok {
|
|
829
|
-
border-left: 3px solid var(--accent-green);
|
|
616
|
+
border-radius: var(--radius);
|
|
617
|
+
padding: 0.875rem 1rem;
|
|
618
|
+
/* The trend band below bleeds to the card's edges. */
|
|
619
|
+
overflow: hidden;
|
|
620
|
+
transition: border-color 0.12s ease;
|
|
830
621
|
}
|
|
831
622
|
|
|
832
623
|
.live-metric-card.status-warning {
|
|
833
|
-
border-
|
|
624
|
+
border-color: var(--amber-line);
|
|
834
625
|
}
|
|
835
626
|
|
|
836
627
|
.live-metric-card.status-critical {
|
|
837
|
-
border-
|
|
628
|
+
border-color: var(--rose-line);
|
|
838
629
|
}
|
|
839
630
|
|
|
840
631
|
.metric-header {
|
|
841
632
|
display: flex;
|
|
842
633
|
align-items: center;
|
|
843
634
|
gap: 0.5rem;
|
|
844
|
-
margin-bottom: 0.
|
|
635
|
+
margin-bottom: 0.25rem;
|
|
845
636
|
}
|
|
846
637
|
|
|
847
638
|
.metric-icon {
|
|
848
|
-
width:
|
|
849
|
-
height:
|
|
850
|
-
|
|
851
|
-
display: flex;
|
|
852
|
-
align-items: center;
|
|
853
|
-
justify-content: center;
|
|
854
|
-
font-size: 0.875rem;
|
|
639
|
+
width: 0.9375rem;
|
|
640
|
+
height: 0.9375rem;
|
|
641
|
+
color: var(--text-muted);
|
|
855
642
|
}
|
|
856
643
|
|
|
857
644
|
.metric-name {
|
|
858
|
-
font-size: 0.
|
|
645
|
+
font-size: 0.75rem;
|
|
859
646
|
font-weight: 500;
|
|
647
|
+
letter-spacing: 0.02em;
|
|
860
648
|
color: var(--text-secondary);
|
|
861
649
|
}
|
|
862
650
|
|
|
863
651
|
.metric-body {
|
|
864
652
|
display: flex;
|
|
865
653
|
flex-direction: column;
|
|
866
|
-
|
|
654
|
+
/* Figures hug the right edge; the trend band below opts back out. */
|
|
655
|
+
align-items: flex-end;
|
|
656
|
+
flex: 1;
|
|
657
|
+
gap: 0.1875rem;
|
|
867
658
|
}
|
|
868
659
|
|
|
869
660
|
.metric-value {
|
|
870
661
|
display: flex;
|
|
871
662
|
align-items: baseline;
|
|
872
|
-
gap: 0.
|
|
663
|
+
gap: 0.3rem;
|
|
873
664
|
}
|
|
874
665
|
|
|
875
666
|
.metric-value span:first-child {
|
|
876
|
-
font-size: 1.
|
|
877
|
-
font-weight:
|
|
667
|
+
font-size: 1.625rem;
|
|
668
|
+
font-weight: 600;
|
|
669
|
+
letter-spacing: -0.02em;
|
|
878
670
|
color: var(--text-primary);
|
|
879
|
-
line-height: 1;
|
|
671
|
+
line-height: 1.1;
|
|
880
672
|
}
|
|
881
673
|
|
|
674
|
+
.status-warning .metric-value span:first-child { color: var(--accent-amber); }
|
|
675
|
+
.status-critical .metric-value span:first-child { color: var(--accent-rose); }
|
|
676
|
+
|
|
882
677
|
.metric-unit {
|
|
883
678
|
font-size: 0.75rem;
|
|
884
679
|
color: var(--text-muted);
|
|
885
680
|
}
|
|
886
681
|
|
|
887
682
|
.metric-detail {
|
|
683
|
+
max-width: 100%;
|
|
888
684
|
font-size: 0.7rem;
|
|
889
685
|
color: var(--text-muted);
|
|
686
|
+
text-align: right;
|
|
687
|
+
white-space: nowrap;
|
|
688
|
+
overflow: hidden;
|
|
689
|
+
text-overflow: ellipsis;
|
|
890
690
|
}
|
|
891
691
|
|
|
892
|
-
/*
|
|
692
|
+
/* Trend band. Negative margins cancel the card's padding so it spans the full
|
|
693
|
+
width and sits flush with the bottom edge — as a floating inset line it left
|
|
694
|
+
the lower half of the card reading as dead space. `margin-top: auto` keeps
|
|
695
|
+
it pinned down when cards in a row differ in height. */
|
|
893
696
|
.metric-sparkline {
|
|
894
|
-
|
|
895
|
-
|
|
697
|
+
/* Full width, regardless of the right-alignment its siblings use. */
|
|
698
|
+
align-self: stretch;
|
|
699
|
+
height: 2.25rem;
|
|
700
|
+
/* auto keeps the band on the bottom edge when cards in a row differ in
|
|
701
|
+
height; the negative side/bottom margins cancel the card's padding. */
|
|
702
|
+
margin: auto -1rem -0.875rem;
|
|
703
|
+
padding-top: 0.5rem;
|
|
704
|
+
color: var(--text-muted);
|
|
896
705
|
}
|
|
897
706
|
|
|
707
|
+
.status-warning .metric-sparkline { color: var(--accent-amber); }
|
|
708
|
+
.status-critical .metric-sparkline { color: var(--accent-rose); }
|
|
709
|
+
|
|
898
710
|
.metric-sparkline svg {
|
|
899
711
|
width: 100%;
|
|
900
712
|
height: 100%;
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
/* Status indicator */
|
|
904
|
-
.metric-status {
|
|
905
|
-
position: absolute;
|
|
906
|
-
top: 0.5rem;
|
|
907
|
-
right: 0.5rem;
|
|
908
|
-
width: 6px;
|
|
909
|
-
height: 6px;
|
|
910
|
-
border-radius: 50%;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
.metric-status.ok { background: var(--accent-green); }
|
|
914
|
-
.metric-status.warning { background: var(--accent-amber); }
|
|
915
|
-
.metric-status.critical { background: var(--accent-rose); }
|
|
916
|
-
|
|
917
|
-
/* Toast Notifications */
|
|
918
|
-
.toast {
|
|
919
|
-
position: fixed;
|
|
920
|
-
bottom: 2rem;
|
|
921
|
-
right: 2rem;
|
|
922
|
-
padding: 1rem 1.5rem;
|
|
923
|
-
background: var(--bg-card);
|
|
924
|
-
border: 1px solid var(--border-color);
|
|
925
|
-
border-radius: 6px;
|
|
926
|
-
color: var(--text-primary);
|
|
927
|
-
font-size: 0.875rem;
|
|
928
|
-
font-weight: 500;
|
|
929
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
930
|
-
z-index: 9999;
|
|
931
|
-
opacity: 0;
|
|
932
|
-
transform: translateY(20px);
|
|
933
|
-
transition: all 0.3s ease;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
.toast.show {
|
|
937
|
-
opacity: 1;
|
|
938
|
-
transform: translateY(0);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
.toast.toast-success {
|
|
942
|
-
border-left: 3px solid var(--accent-green);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
.toast.toast-error {
|
|
946
|
-
border-left: 3px solid var(--accent-rose);
|
|
947
|
-
background: rgba(244, 63, 94, 0.1);
|
|
948
|
-
border-color: var(--accent-rose);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
.toast.toast-warning {
|
|
952
|
-
border-left: 3px solid var(--accent-amber);
|
|
713
|
+
display: block;
|
|
953
714
|
}
|
|
954
715
|
|
|
955
716
|
/* Run Query Modal */
|
|
@@ -960,62 +721,57 @@ end
|
|
|
960
721
|
|
|
961
722
|
.explain-label {
|
|
962
723
|
display: block;
|
|
963
|
-
font-size: 0.
|
|
724
|
+
font-size: 0.6875rem;
|
|
964
725
|
font-weight: 600;
|
|
726
|
+
line-height: 1.4;
|
|
727
|
+
letter-spacing: 0.06em;
|
|
965
728
|
color: var(--text-muted);
|
|
966
729
|
text-transform: uppercase;
|
|
967
|
-
margin-bottom: 0.
|
|
730
|
+
margin-bottom: 0.4rem;
|
|
968
731
|
}
|
|
969
732
|
|
|
970
733
|
.run-query-textarea {
|
|
971
734
|
width: 100%;
|
|
972
735
|
min-height: 180px;
|
|
973
|
-
padding:
|
|
736
|
+
padding: 0.875rem;
|
|
974
737
|
background: var(--bg-primary);
|
|
975
738
|
border: 1px solid var(--border-color);
|
|
976
|
-
border-radius:
|
|
739
|
+
border-radius: var(--radius-sm);
|
|
977
740
|
color: var(--text-primary);
|
|
978
|
-
font-family:
|
|
979
|
-
font-size: 0.
|
|
980
|
-
line-height: 1.
|
|
741
|
+
font-family: var(--font-mono);
|
|
742
|
+
font-size: 0.8125rem;
|
|
743
|
+
line-height: 1.6;
|
|
981
744
|
resize: vertical;
|
|
982
745
|
}
|
|
983
746
|
|
|
984
|
-
.run-query-textarea:focus {
|
|
985
|
-
outline:
|
|
747
|
+
.run-query-textarea:focus-visible {
|
|
748
|
+
outline: 2px solid var(--accent-blue);
|
|
749
|
+
outline-offset: -1px;
|
|
986
750
|
border-color: var(--accent-blue);
|
|
987
|
-
box-shadow: 0 0 0 2px rgba(107, 159, 232, 0.2);
|
|
988
751
|
}
|
|
989
752
|
|
|
990
753
|
.run-query-textarea::placeholder {
|
|
991
754
|
color: var(--text-muted);
|
|
992
|
-
font-style: italic;
|
|
993
755
|
}
|
|
994
756
|
|
|
995
757
|
.run-query-actions {
|
|
996
758
|
display: flex;
|
|
997
|
-
gap: 0.
|
|
998
|
-
margin: 1rem 0;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
.run-query-actions .btn {
|
|
1002
|
-
flex: 1;
|
|
759
|
+
gap: 0.5rem;
|
|
1003
760
|
}
|
|
1004
761
|
|
|
1005
762
|
/* Query Results Table (mirrors _show_styles.html.erb — index.html.erb has
|
|
1006
763
|
its own <style> block and isn't loaded through that partial) */
|
|
1007
764
|
.query-results-wrapper {
|
|
1008
|
-
margin-top: 1rem;
|
|
1009
765
|
max-height: 400px;
|
|
1010
766
|
overflow: auto;
|
|
1011
767
|
border: 1px solid var(--border-color);
|
|
1012
|
-
border-radius:
|
|
768
|
+
border-radius: var(--radius);
|
|
1013
769
|
}
|
|
1014
770
|
|
|
1015
771
|
.query-results-table {
|
|
1016
772
|
width: 100%;
|
|
1017
773
|
border-collapse: collapse;
|
|
1018
|
-
font-family:
|
|
774
|
+
font-family: var(--font-mono);
|
|
1019
775
|
font-size: 0.75rem;
|
|
1020
776
|
}
|
|
1021
777
|
|
|
@@ -1054,8 +810,7 @@ end
|
|
|
1054
810
|
justify-content: space-between;
|
|
1055
811
|
padding: 0.75rem 1rem;
|
|
1056
812
|
background: var(--bg-tertiary);
|
|
1057
|
-
border-radius:
|
|
1058
|
-
margin-top: 1rem;
|
|
813
|
+
border-radius: var(--radius);
|
|
1059
814
|
font-size: 0.8rem;
|
|
1060
815
|
color: var(--text-secondary);
|
|
1061
816
|
}
|
|
@@ -1121,15 +876,7 @@ end
|
|
|
1121
876
|
|
|
1122
877
|
if (!allowRawQueryExecution) {
|
|
1123
878
|
showToast(PG_REPORTS_I18N.errors.execute_disabled_toast, 'error');
|
|
1124
|
-
content.innerHTML =
|
|
1125
|
-
<strong>${PG_REPORTS_I18N.modals.query_execution_disabled_title}</strong><br><br>
|
|
1126
|
-
${PG_REPORTS_I18N.modals.query_execution_disabled_intro}<br>
|
|
1127
|
-
<code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
|
|
1128
|
-
PgReports.configure do |config|<br>
|
|
1129
|
-
config.allow_raw_query_execution = true<br>
|
|
1130
|
-
end
|
|
1131
|
-
</code>
|
|
1132
|
-
</div>`;
|
|
879
|
+
content.innerHTML = pgReportsDisabledNotice(PG_REPORTS_I18N.modals.query_execution_disabled_title);
|
|
1133
880
|
return;
|
|
1134
881
|
}
|
|
1135
882
|
|
|
@@ -1425,14 +1172,12 @@ end
|
|
|
1425
1172
|
function updateToggleUI() {
|
|
1426
1173
|
const icon = document.getElementById('toggle-icon');
|
|
1427
1174
|
const indicator = document.querySelector('.live-indicator');
|
|
1175
|
+
const use = icon && icon.querySelector('use');
|
|
1428
1176
|
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1431
|
-
indicator.classList.remove('paused');
|
|
1432
|
-
} else {
|
|
1433
|
-
icon.textContent = '▶';
|
|
1434
|
-
indicator.classList.add('paused');
|
|
1177
|
+
if (use) {
|
|
1178
|
+
use.setAttribute('href', liveMonitoringEnabled ? '#i-pause' : '#i-play');
|
|
1435
1179
|
}
|
|
1180
|
+
indicator.classList.toggle('paused', !liveMonitoringEnabled);
|
|
1436
1181
|
}
|
|
1437
1182
|
|
|
1438
1183
|
async function fetchLiveMetrics() {
|
|
@@ -1505,18 +1250,18 @@ end
|
|
|
1505
1250
|
addToHistory('longrunning', metrics.long_running_count);
|
|
1506
1251
|
addToHistory('blocked', metrics.blocked_count);
|
|
1507
1252
|
|
|
1508
|
-
renderSparkline('connections', metricsHistory.connections, 'var(--accent-blue)');
|
|
1509
|
-
renderSparkline('tps', metricsHistory.tps, 'var(--accent-green)');
|
|
1510
|
-
renderSparkline('cache', metricsHistory.cache, 'var(--accent-purple)');
|
|
1511
|
-
renderSparkline('longrunning', metricsHistory.longrunning, 'var(--accent-amber)');
|
|
1512
|
-
renderSparkline('blocked', metricsHistory.blocked, 'var(--accent-rose)');
|
|
1513
|
-
|
|
1514
1253
|
updateStatus('connections', metrics.connections.percent, 'higher');
|
|
1515
1254
|
updateStatus('tps', tps, null);
|
|
1516
1255
|
updateStatus('cache', metrics.cache_hit_ratio, 'lower');
|
|
1517
1256
|
updateStatus('longrunning', metrics.long_running_count, 'higher');
|
|
1518
1257
|
updateStatus('blocked', metrics.blocked_count, 'higher');
|
|
1519
1258
|
|
|
1259
|
+
renderSparkline('connections', metricsHistory.connections);
|
|
1260
|
+
renderSparkline('tps', metricsHistory.tps);
|
|
1261
|
+
renderSparkline('cache', metricsHistory.cache);
|
|
1262
|
+
renderSparkline('longrunning', metricsHistory.longrunning);
|
|
1263
|
+
renderSparkline('blocked', metricsHistory.blocked);
|
|
1264
|
+
|
|
1520
1265
|
previousMetrics = metrics;
|
|
1521
1266
|
}
|
|
1522
1267
|
|
|
@@ -1533,7 +1278,7 @@ end
|
|
|
1533
1278
|
return num.toString();
|
|
1534
1279
|
}
|
|
1535
1280
|
|
|
1536
|
-
function renderSparkline(metric, data
|
|
1281
|
+
function renderSparkline(metric, data) {
|
|
1537
1282
|
const svg = document.getElementById(`sparkline-${metric}`);
|
|
1538
1283
|
if (!svg || data.length < 2) return;
|
|
1539
1284
|
|
|
@@ -1541,35 +1286,43 @@ end
|
|
|
1541
1286
|
|
|
1542
1287
|
const width = 100;
|
|
1543
1288
|
const height = 30;
|
|
1289
|
+
// No horizontal inset: the band spans the card's full width, so the trace
|
|
1290
|
+
// should reach both edges. Vertical inset only, to keep the stroke from
|
|
1291
|
+
// being clipped at the extremes.
|
|
1544
1292
|
const padding = 2;
|
|
1545
1293
|
|
|
1546
1294
|
const min = Math.min(...data);
|
|
1547
1295
|
const max = Math.max(...data);
|
|
1548
|
-
const range = max - min
|
|
1296
|
+
const range = max - min;
|
|
1549
1297
|
|
|
1550
1298
|
const points = data.map((value, index) => {
|
|
1551
|
-
const x =
|
|
1552
|
-
|
|
1299
|
+
const x = (index / (data.length - 1)) * width;
|
|
1300
|
+
// A series that never moves is drawn mid-band: pinned to the floor it
|
|
1301
|
+
// reads as "zero" (or as a stray underline) rather than "steady".
|
|
1302
|
+
const y = range === 0
|
|
1303
|
+
? height / 2
|
|
1304
|
+
: height - padding - ((value - min) / range) * (height - 2 * padding);
|
|
1553
1305
|
return { x, y };
|
|
1554
1306
|
});
|
|
1555
1307
|
|
|
1556
1308
|
const pathD = points.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(' ');
|
|
1557
1309
|
|
|
1558
1310
|
const areaD = pathD +
|
|
1559
|
-
` L ${points[points.length - 1].x} ${height
|
|
1560
|
-
` L ${points[0].x} ${height
|
|
1311
|
+
` L ${points[points.length - 1].x} ${height}` +
|
|
1312
|
+
` L ${points[0].x} ${height} Z`;
|
|
1561
1313
|
|
|
1562
1314
|
const area = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1563
1315
|
area.setAttribute('d', areaD);
|
|
1564
|
-
area.setAttribute('fill',
|
|
1565
|
-
area.setAttribute('opacity', '0.
|
|
1316
|
+
area.setAttribute('fill', 'currentColor');
|
|
1317
|
+
area.setAttribute('opacity', '0.12');
|
|
1566
1318
|
svg.appendChild(area);
|
|
1567
1319
|
|
|
1568
1320
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1569
1321
|
path.setAttribute('d', pathD);
|
|
1570
1322
|
path.setAttribute('fill', 'none');
|
|
1571
|
-
path.setAttribute('stroke',
|
|
1572
|
-
path.setAttribute('stroke-width', '1.
|
|
1323
|
+
path.setAttribute('stroke', 'currentColor');
|
|
1324
|
+
path.setAttribute('stroke-width', '1.25');
|
|
1325
|
+
path.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
1573
1326
|
path.setAttribute('stroke-linecap', 'round');
|
|
1574
1327
|
path.setAttribute('stroke-linejoin', 'round');
|
|
1575
1328
|
svg.appendChild(path);
|
|
@@ -1577,28 +1330,23 @@ end
|
|
|
1577
1330
|
|
|
1578
1331
|
function updateStatus(metric, value, badDirection) {
|
|
1579
1332
|
const card = document.querySelector(`[data-metric="${metric}"]`);
|
|
1580
|
-
|
|
1581
|
-
if (!card || !statusDot) return;
|
|
1333
|
+
if (!card) return;
|
|
1582
1334
|
|
|
1583
1335
|
const thresholds = LIVE_CONFIG.thresholds[metric];
|
|
1584
|
-
if (!thresholds.warning && !thresholds.critical) {
|
|
1585
|
-
card.className = 'live-metric-card status-ok';
|
|
1586
|
-
statusDot.className = 'metric-status ok';
|
|
1587
|
-
return;
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
1336
|
let status = 'ok';
|
|
1591
1337
|
|
|
1592
|
-
if (
|
|
1593
|
-
if (
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
if (
|
|
1597
|
-
|
|
1338
|
+
if (thresholds.warning || thresholds.critical) {
|
|
1339
|
+
if (badDirection === 'higher') {
|
|
1340
|
+
if (value >= thresholds.critical) status = 'critical';
|
|
1341
|
+
else if (value >= thresholds.warning) status = 'warning';
|
|
1342
|
+
} else if (badDirection === 'lower') {
|
|
1343
|
+
if (value <= thresholds.critical) status = 'critical';
|
|
1344
|
+
else if (value <= thresholds.warning) status = 'warning';
|
|
1345
|
+
}
|
|
1598
1346
|
}
|
|
1599
1347
|
|
|
1600
|
-
card.
|
|
1601
|
-
|
|
1348
|
+
card.classList.remove('status-ok', 'status-warning', 'status-critical');
|
|
1349
|
+
card.classList.add(`status-${status}`);
|
|
1602
1350
|
}
|
|
1603
1351
|
|
|
1604
1352
|
document.addEventListener('DOMContentLoaded', initLiveMonitoring);
|
|
@@ -1620,10 +1368,6 @@ end
|
|
|
1620
1368
|
'cursor': 5
|
|
1621
1369
|
};
|
|
1622
1370
|
|
|
1623
|
-
function getDefaultIde() {
|
|
1624
|
-
return localStorage.getItem('pgReportsDefaultIde') || '';
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
1371
|
function generateIdeUrls(filePath, lineNumber) {
|
|
1628
1372
|
if (!filePath) return [];
|
|
1629
1373
|
|
|
@@ -1722,41 +1466,6 @@ end
|
|
|
1722
1466
|
document.getElementById('ide-dropdown-overlay')?.classList.remove('show');
|
|
1723
1467
|
}
|
|
1724
1468
|
|
|
1725
|
-
function showIdeSettingsModal() {
|
|
1726
|
-
const modal = document.getElementById('ide-settings-modal');
|
|
1727
|
-
modal.style.display = 'flex';
|
|
1728
|
-
loadIdeSettingsState();
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
function closeIdeSettingsModal() {
|
|
1732
|
-
document.getElementById('ide-settings-modal').style.display = 'none';
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
function setDefaultIde(ideKey) {
|
|
1736
|
-
if (ideKey) {
|
|
1737
|
-
localStorage.setItem('pgReportsDefaultIde', ideKey);
|
|
1738
|
-
} else {
|
|
1739
|
-
localStorage.removeItem('pgReportsDefaultIde');
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
function loadIdeSettingsState() {
|
|
1744
|
-
const currentIde = getDefaultIde();
|
|
1745
|
-
const radios = document.querySelectorAll('input[name="default-ide"]');
|
|
1746
|
-
radios.forEach(radio => {
|
|
1747
|
-
radio.checked = (radio.value === currentIde);
|
|
1748
|
-
});
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
// Close IDE settings modal on backdrop click
|
|
1752
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
1753
|
-
document.getElementById('ide-settings-modal')?.addEventListener('click', function(e) {
|
|
1754
|
-
if (e.target === this) {
|
|
1755
|
-
closeIdeSettingsModal();
|
|
1756
|
-
}
|
|
1757
|
-
});
|
|
1758
|
-
});
|
|
1759
|
-
|
|
1760
1469
|
// ============================================
|
|
1761
1470
|
// Query Monitoring
|
|
1762
1471
|
// ============================================
|
|
@@ -1784,6 +1493,8 @@ end
|
|
|
1784
1493
|
const response = await fetch(`${pgReportsRoot}/query_monitor/status`);
|
|
1785
1494
|
const data = await response.json();
|
|
1786
1495
|
|
|
1496
|
+
queryHistoryAvailable = data.history_available === true;
|
|
1497
|
+
|
|
1787
1498
|
if (data.success && data.enabled) {
|
|
1788
1499
|
queryMonitorEnabled = true;
|
|
1789
1500
|
currentSessionId = data.session_id;
|
|
@@ -1802,6 +1513,17 @@ end
|
|
|
1802
1513
|
}
|
|
1803
1514
|
}
|
|
1804
1515
|
|
|
1516
|
+
// Set from /query_monitor/status; null until the first check answers.
|
|
1517
|
+
let queryHistoryAvailable = null;
|
|
1518
|
+
|
|
1519
|
+
function applyHistoryAvailability(button) {
|
|
1520
|
+
const btn = button || document.getElementById('load-history-btn');
|
|
1521
|
+
if (!btn || queryHistoryAvailable === null) return;
|
|
1522
|
+
|
|
1523
|
+
btn.disabled = !queryHistoryAvailable;
|
|
1524
|
+
btn.title = queryHistoryAvailable ? '' : PG_REPORTS_I18N.actions.load_history_unavailable_title;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1805
1527
|
async function loadQueryHistory(button) {
|
|
1806
1528
|
button.disabled = true;
|
|
1807
1529
|
button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.loading;
|
|
@@ -1822,8 +1544,9 @@ end
|
|
|
1822
1544
|
console.error('Failed to load query history:', error);
|
|
1823
1545
|
showToast(PG_REPORTS_I18N.errors.load_history_failed + ' ' + error.message, 'error');
|
|
1824
1546
|
} finally {
|
|
1825
|
-
button.disabled = false;
|
|
1826
1547
|
button.innerHTML = PG_REPORTS_I18N.actions.load_history;
|
|
1548
|
+
button.disabled = false;
|
|
1549
|
+
applyHistoryAvailability(button);
|
|
1827
1550
|
}
|
|
1828
1551
|
}
|
|
1829
1552
|
|
|
@@ -1924,6 +1647,7 @@ end
|
|
|
1924
1647
|
startBtn.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
|
|
1925
1648
|
stopBtn.style.display = 'none';
|
|
1926
1649
|
loadHistoryBtn.style.display = 'inline-block'; // Show when monitoring stopped
|
|
1650
|
+
applyHistoryAvailability(loadHistoryBtn);
|
|
1927
1651
|
sessionBadge.style.display = 'none';
|
|
1928
1652
|
|
|
1929
1653
|
// Keep results visible after stopping, only hide download if no queries
|