pg_reports 0.7.0 → 0.8.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.
@@ -113,13 +113,32 @@
113
113
  display: flex;
114
114
  align-items: center;
115
115
  gap: 0.4rem;
116
- padding: 0.4rem 0.75rem;
116
+ height: 2.125rem;
117
+ padding: 0 0.75rem;
117
118
  background: var(--bg-tertiary);
118
119
  border: 1px solid var(--border-color);
119
120
  border-radius: 4px;
120
121
  font-size: 0.75rem;
121
122
  }
122
123
 
124
+ .header-badge-clickable {
125
+ cursor: pointer;
126
+ font-family: inherit;
127
+ color: inherit;
128
+ transition: border-color 0.15s ease, background 0.15s ease;
129
+ }
130
+
131
+ .header-badge-clickable:hover {
132
+ background: var(--bg-card);
133
+ border-color: var(--text-muted);
134
+ }
135
+
136
+ .badge-info-icon {
137
+ color: var(--text-muted);
138
+ font-size: 0.85rem;
139
+ line-height: 1;
140
+ }
141
+
123
142
  .badge-dot {
124
143
  width: 8px;
125
144
  height: 8px;
@@ -306,13 +325,14 @@
306
325
  }
307
326
 
308
327
  .btn-primary {
309
- background: var(--accent-purple);
310
- color: #fff;
311
- border-color: var(--accent-purple);
328
+ background: rgba(124, 138, 246, 0.14);
329
+ color: var(--accent-purple);
330
+ border-color: rgba(124, 138, 246, 0.35);
312
331
  }
313
332
 
314
333
  .btn-primary:hover {
315
- background: #6b79e4;
334
+ background: rgba(124, 138, 246, 0.24);
335
+ border-color: rgba(124, 138, 246, 0.55);
316
336
  }
317
337
 
318
338
  .btn-secondary {
@@ -633,6 +653,51 @@
633
653
  font-size: 0.9rem;
634
654
  }
635
655
 
656
+ .query-monitoring-scope {
657
+ position: relative;
658
+ font-weight: 400;
659
+ font-size: 0.75rem;
660
+ color: var(--text-muted);
661
+ background: var(--bg-tertiary);
662
+ padding: 0.125rem 0.5rem;
663
+ border-radius: 999px;
664
+ cursor: help;
665
+ }
666
+
667
+ .query-monitoring-scope::after {
668
+ content: attr(data-tooltip);
669
+ position: absolute;
670
+ top: calc(100% + 8px);
671
+ left: 0;
672
+ z-index: 100;
673
+ width: max-content;
674
+ max-width: 320px;
675
+ padding: 0.6rem 0.75rem;
676
+ background: var(--bg-card);
677
+ color: var(--text-secondary);
678
+ border: 1px solid var(--border-color);
679
+ border-radius: 6px;
680
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
681
+ font-size: 0.75rem;
682
+ font-weight: 400;
683
+ line-height: 1.5;
684
+ white-space: normal;
685
+ text-align: left;
686
+ opacity: 0;
687
+ visibility: hidden;
688
+ transform: translateY(-4px);
689
+ transition: opacity 0.15s ease, transform 0.15s ease;
690
+ pointer-events: none;
691
+ }
692
+
693
+ .query-monitoring-scope:hover::after,
694
+ .query-monitoring-scope:focus::after,
695
+ .query-monitoring-scope:focus-visible::after {
696
+ opacity: 1;
697
+ visibility: visible;
698
+ transform: translateY(0);
699
+ }
700
+
636
701
  .monitoring-indicator {
637
702
  width: 7px;
638
703
  height: 7px;
@@ -989,6 +1054,125 @@
989
1054
  .modal-small {
990
1055
  max-width: 360px;
991
1056
  }
1057
+
1058
+ .modal-medium {
1059
+ max-width: 560px;
1060
+ }
1061
+
1062
+ /* Database selector — used inside .live-monitoring-title and .breadcrumb.
1063
+ Sizes to fit its longest option (no artificial cap), capped at the
1064
+ parent's available width so it never overruns neighboring controls. */
1065
+ .db-selector-form {
1066
+ display: inline-flex;
1067
+ align-items: center;
1068
+ gap: 0.5rem;
1069
+ margin: 0;
1070
+ max-width: 100%;
1071
+ min-width: 0;
1072
+ }
1073
+
1074
+ .db-selector-label {
1075
+ font-size: 0.75rem;
1076
+ color: var(--text-secondary);
1077
+ text-transform: uppercase;
1078
+ letter-spacing: 0.5px;
1079
+ font-weight: 600;
1080
+ white-space: nowrap;
1081
+ }
1082
+
1083
+ .db-selector {
1084
+ background: var(--bg-card, rgba(255, 255, 255, 0.04));
1085
+ color: var(--text-primary);
1086
+ border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
1087
+ border-radius: 6px;
1088
+ padding: 0.375rem 0.625rem;
1089
+ font-size: 0.875rem;
1090
+ cursor: pointer;
1091
+ width: auto;
1092
+ max-width: 100%;
1093
+ min-width: 0;
1094
+ }
1095
+
1096
+ .db-selector option.is-default {
1097
+ font-weight: 700;
1098
+ }
1099
+
1100
+ .db-selector:hover {
1101
+ border-color: var(--accent-blue, #6b9fe8);
1102
+ }
1103
+
1104
+ .db-selector:focus {
1105
+ outline: none;
1106
+ border-color: var(--accent-blue, #6b9fe8);
1107
+ box-shadow: 0 0 0 2px rgba(107, 159, 232, 0.2);
1108
+ }
1109
+
1110
+ .db-selector-static {
1111
+ display: inline-flex;
1112
+ align-items: center;
1113
+ gap: 0.5rem;
1114
+ font-size: 0.875rem;
1115
+ color: var(--text-secondary);
1116
+ }
1117
+
1118
+ .db-selector-static strong {
1119
+ color: var(--text-primary);
1120
+ }
1121
+
1122
+ .db-selector-error {
1123
+ display: inline-flex;
1124
+ align-items: center;
1125
+ gap: 0.375rem;
1126
+ padding: 0.375rem 0.625rem;
1127
+ background: rgba(232, 107, 107, 0.1);
1128
+ border: 1px solid rgba(232, 107, 107, 0.3);
1129
+ border-radius: 6px;
1130
+ font-size: 0.8125rem;
1131
+ color: var(--accent-red, #e86b6b);
1132
+ cursor: help;
1133
+ }
1134
+
1135
+ .breadcrumb-spacer {
1136
+ flex: 1;
1137
+ }
1138
+
1139
+ .breadcrumb {
1140
+ display: flex;
1141
+ align-items: center;
1142
+ gap: 0.5rem;
1143
+ }
1144
+
1145
+ .db-error-banner {
1146
+ background: rgba(232, 107, 107, 0.08);
1147
+ border: 1px solid rgba(232, 107, 107, 0.3);
1148
+ border-radius: 8px;
1149
+ padding: 0.875rem 1.125rem;
1150
+ margin-bottom: 1rem;
1151
+ display: flex;
1152
+ flex-direction: column;
1153
+ gap: 0.375rem;
1154
+ }
1155
+
1156
+ .db-error-banner strong {
1157
+ color: var(--accent-red, #e86b6b);
1158
+ font-size: 0.9375rem;
1159
+ }
1160
+
1161
+ .db-error-banner span {
1162
+ font-size: 0.875rem;
1163
+ color: var(--text-secondary);
1164
+ }
1165
+
1166
+ .db-error-banner code {
1167
+ font-family: ui-monospace, SFMono-Regular, monospace;
1168
+ font-size: 0.8125rem;
1169
+ background: rgba(0, 0, 0, 0.25);
1170
+ padding: 0.375rem 0.625rem;
1171
+ border-radius: 4px;
1172
+ color: var(--text-primary);
1173
+ display: inline-block;
1174
+ width: fit-content;
1175
+ }
992
1176
  </style>
993
1177
  </head>
994
1178
  <body>
@@ -1003,7 +1187,10 @@
1003
1187
  <div id="ide-menu" class="ide-menu"></div>
1004
1188
 
1005
1189
  <script>
1006
- const pgReportsRoot = document.querySelector('meta[name="pg-reports-root"]')?.content || '/pg_reports';
1190
+ // Trailing slash stripped so `${pgReportsRoot}/foo` never becomes `//foo`
1191
+ // a protocol-relative URL (host "foo") — when the engine is mounted at "/"
1192
+ // (e.g. standalone mode), where the mount path resolves to "/".
1193
+ const pgReportsRoot = (document.querySelector('meta[name="pg-reports-root"]')?.content || '/pg_reports').replace(/\/+$/, '');
1007
1194
  window.PG_REPORTS_I18N = <%= raw I18n.t("pg_reports.ui").to_json %>;
1008
1195
 
1009
1196
  // Format strings with %{var} placeholders
@@ -1012,6 +1199,26 @@
1012
1199
  return template.replace(/%\{(\w+)\}/g, function(_, k) { return vars[k] != null ? vars[k] : ''; });
1013
1200
  };
1014
1201
 
1202
+ // Close the topmost open modal on ESC. Applies to every `.modal` that has a
1203
+ // close (×) button — it triggers that button so each modal's own close
1204
+ // handler (and any cleanup) runs.
1205
+ document.addEventListener('keydown', function(e) {
1206
+ if (e.key !== 'Escape' && e.key !== 'Esc') return;
1207
+ const open = Array.prototype.slice.call(document.querySelectorAll('.modal'))
1208
+ .filter(function(m) {
1209
+ return getComputedStyle(m).display !== 'none' && m.querySelector('.modal-close');
1210
+ });
1211
+ if (!open.length) return;
1212
+ e.preventDefault();
1213
+ const modal = open[open.length - 1];
1214
+ const closeBtn = modal.querySelector('.modal-close');
1215
+ if (closeBtn) {
1216
+ closeBtn.click();
1217
+ } else {
1218
+ modal.style.display = 'none';
1219
+ }
1220
+ });
1221
+
1015
1222
  function showToast(message, type = 'success') {
1016
1223
  const toast = document.getElementById('toast');
1017
1224
  toast.textContent = message;
@@ -0,0 +1,39 @@
1
+ <%# locals: (show_label: true) %>
2
+ <% if @available_databases.present? && @available_databases.size > 1 %>
3
+ <form class="db-selector-form"
4
+ action="<%= switch_database_path %>"
5
+ method="post"
6
+ data-turbo="false">
7
+ <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
8
+ <% if local_assigns.fetch(:show_label, true) %>
9
+ <label class="db-selector-label" for="pg-reports-db-select">
10
+ <%= t("pg_reports.ui.database_selector.label", default: "Database") %>
11
+ </label>
12
+ <% end %>
13
+ <select id="pg-reports-db-select"
14
+ name="database"
15
+ class="db-selector"
16
+ onchange="this.form.submit()">
17
+ <% @available_databases.each do |row| %>
18
+ <option value="<%= row["name"] %>"
19
+ class="<%= "is-default" if row["name"] == @target_default_database %>"
20
+ <%= "selected" if row["name"] == @selected_database %>>
21
+ <%= row["name"] %><%= " (#{row["size"]})" if row["size"].present? %>
22
+ </option>
23
+ <% end %>
24
+ </select>
25
+ </form>
26
+ <% elsif @available_databases.present? && @available_databases.size == 1 %>
27
+ <span class="db-selector-static">
28
+ <% if local_assigns.fetch(:show_label, true) %>
29
+ <span class="db-selector-label">
30
+ <%= t("pg_reports.ui.database_selector.label", default: "Database") %>:
31
+ </span>
32
+ <% end %>
33
+ <strong><%= @available_databases.first["name"] %></strong>
34
+ </span>
35
+ <% elsif @database_error %>
36
+ <span class="db-selector-error" title="<%= @database_error[:detail] %>">
37
+ ⚠ <%= @database_error[:title] %>
38
+ </span>
39
+ <% end %>
@@ -0,0 +1,27 @@
1
+ <%# locals: (show_label: true) %>
2
+ <% if @available_targets.present? && @available_targets.size > 1 %>
3
+ <form class="db-selector-form"
4
+ action="<%= switch_target_path %>"
5
+ method="post"
6
+ data-turbo="false">
7
+ <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
8
+ <% if local_assigns.fetch(:show_label, true) %>
9
+ <label class="db-selector-label" for="pg-reports-target-select">
10
+ <%= t("pg_reports.ui.target_selector.label", default: "Target") %>
11
+ </label>
12
+ <% end %>
13
+ <select id="pg-reports-target-select"
14
+ name="target"
15
+ class="db-selector"
16
+ onchange="this.form.submit()">
17
+ <% default_target_name = PgReports.connection_registry.default_name %>
18
+ <% @available_targets.each do |target| %>
19
+ <option value="<%= target.name %>"
20
+ class="<%= "is-default" if target.name == default_target_name %>"
21
+ <%= "selected" if target.name == @selected_target %>>
22
+ <%= target.name %><%= " (#{target.default_database})" if target.default_database.present? %>
23
+ </option>
24
+ <% end %>
25
+ </select>
26
+ </form>
27
+ <% end %>
@@ -1,5 +1,15 @@
1
1
  <%= csrf_meta_tags %>
2
2
 
3
+ <% if @database_error %>
4
+ <div class="db-error-banner">
5
+ <strong><%= @database_error[:title] %></strong>
6
+ <span><%= @database_error[:detail] %></span>
7
+ <% if @database_error[:hint] %>
8
+ <code><%= @database_error[:hint] %></code>
9
+ <% end %>
10
+ </div>
11
+ <% end %>
12
+
3
13
  <header class="header">
4
14
  <div class="logo">
5
15
  <div class="logo-icon">🐘</div>
@@ -17,28 +27,34 @@
17
27
  <button class="btn btn-small btn-muted" onclick="showResetConfirmModal()" id="reset-btn">
18
28
  <%= t("pg_reports.ui.actions.reset_statistics") %>
19
29
  </button>
20
- <% else %>
21
- <button class="btn btn-small btn-primary" onclick="enablePgStatStatements(this)" id="enable-btn">
22
- <%= t("pg_reports.ui.actions.create_extension") %>
23
- </button>
24
- <button class="btn-info" onclick="showPgStatInfo()">?</button>
25
30
  <% end %>
26
31
  <button class="btn-info" onclick="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>">⚙️</button>
27
- <div class="header-badge" id="pg-stat-badge">
28
- <% if @pg_stat_status[:ready] %>
32
+
33
+ <% if !@pg_stat_status[:connected] %>
34
+ <div class="header-badge" id="pg-stat-badge">
35
+ <span class="badge-dot error"></span>
36
+ <span><%= t("pg_reports.ui.status.disconnected") %></span>
37
+ </div>
38
+ <% elsif @pg_stat_status[:ready] %>
39
+ <div class="header-badge" id="pg-stat-badge">
29
40
  <span class="badge-dot"></span>
30
41
  <span><%= t("pg_reports.ui.status.pg_stat_ready") %></span>
31
- <% elsif @pg_stat_status[:extension_installed] %>
42
+ </div>
43
+ <% elsif @pg_stat_status[:extension_installed] %>
44
+ <button type="button" class="header-badge header-badge-clickable" id="pg-stat-badge"
45
+ onclick="showPgStatInfo()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
32
46
  <span class="badge-dot warning"></span>
33
- <span><%= t("pg_reports.ui.status.extension_installed") %></span>
34
- <% elsif @pg_stat_status[:preloaded] %>
47
+ <span><%= t("pg_reports.ui.status.not_preloaded") %></span>
48
+ <span class="badge-info-icon" aria-hidden="true">ⓘ</span>
49
+ </button>
50
+ <% else %>
51
+ <button type="button" class="header-badge header-badge-clickable" id="pg-stat-badge"
52
+ onclick="showCreateExtensionModal()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
35
53
  <span class="badge-dot warning"></span>
36
- <span><%= t("pg_reports.ui.status.preloaded") %></span>
37
- <% else %>
38
- <span class="badge-dot error"></span>
39
- <span><%= t("pg_reports.ui.status.not_configured") %></span>
40
- <% end %>
41
- </div>
54
+ <span><%= t("pg_reports.ui.status.extension_missing") %></span>
55
+ <span class="badge-info-icon" aria-hidden="true">ⓘ</span>
56
+ </button>
57
+ <% end %>
42
58
  </div>
43
59
  </header>
44
60
 
@@ -71,6 +87,25 @@ pg_stat_statements.track = all</pre>
71
87
  </div>
72
88
  </div>
73
89
 
90
+ <!-- Create extension modal -->
91
+ <div id="create-extension-modal" class="modal" style="display: none;">
92
+ <div class="modal-content modal-medium">
93
+ <div class="modal-header">
94
+ <h3><%= t("pg_reports.ui.modals.create_extension_title") %></h3>
95
+ <button class="modal-close" onclick="closeCreateExtensionModal()">×</button>
96
+ </div>
97
+ <div class="modal-body">
98
+ <p><%= t("pg_reports.ui.modals.create_extension_intro") %></p>
99
+ <pre>CREATE EXTENSION IF NOT EXISTS pg_stat_statements;</pre>
100
+ <p class="warning-subtext"><%= t("pg_reports.ui.modals.create_extension_note") %></p>
101
+ <div class="modal-actions">
102
+ <button class="btn btn-secondary" onclick="closeCreateExtensionModal()"><%= t("pg_reports.ui.actions.cancel") %></button>
103
+ <button class="btn btn-primary" onclick="enablePgStatStatements(this)" id="modal-create-extension-btn"><%= t("pg_reports.ui.actions.create_extension") %></button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
74
109
  <!-- Reset statistics confirmation modal -->
75
110
  <div id="reset-confirm-modal" class="modal" style="display: none;">
76
111
  <div class="modal-content modal-small">
@@ -138,9 +173,8 @@ pg_stat_statements.track = all</pre>
138
173
  <div class="live-monitoring-title">
139
174
  <span class="live-indicator"></span>
140
175
  <span><%= t("pg_reports.ui.monitoring.live_title") %></span>
141
- <span class="database-badge">
142
- <strong><%= @current_database %></strong>
143
- </span>
176
+ <%= render "target_selector", show_label: false %>
177
+ <%= render "database_selector", show_label: false %>
144
178
  </div>
145
179
  <div class="live-monitoring-controls">
146
180
  <span class="live-monitoring-interval"><%= t("pg_reports.ui.monitoring.update_interval") %></span>
@@ -258,6 +292,9 @@ pg_stat_statements.track = all</pre>
258
292
  <div class="query-monitoring-title">
259
293
  <span class="monitoring-indicator" id="monitor-indicator"></span>
260
294
  <span><%= t("pg_reports.ui.monitoring.query_monitor_title") %></span>
295
+ <span class="query-monitoring-scope" tabindex="0" data-tooltip="<%= t("pg_reports.ui.monitoring.scope_note_long", default: "Query Monitor subscribes to ActiveSupport::Notifications in the host application's process. It captures every SQL the host app runs, on whichever database the host app is connected to. The dashboard's database selector does not change this scope.") %>">
296
+ <%= t("pg_reports.ui.monitoring.scope_note_short", default: "scope: host application") %>
297
+ </span>
261
298
  <span class="session-badge" id="session-badge" style="display: none;">
262
299
  <%= t("pg_reports.ui.monitoring.session_label") %> <strong id="session-id"></strong>
263
300
  </span>
@@ -297,11 +334,20 @@ pg_stat_statements.track = all</pre>
297
334
 
298
335
  <div class="categories-grid">
299
336
  <% @categories.each do |category_key, category| %>
300
- <div class="category-card<%= ' disabled' if category_key == :queries && !@pg_stat_status[:ready] %>">
301
- <% if category_key == :queries && !@pg_stat_status[:ready] %>
337
+ <%
338
+ requires_pg_stat = (category_key == :queries && !@pg_stat_status[:ready])
339
+ target_disabled_reason = category_disabled_reason(category_key)
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 %>
302
344
  <div class="category-warning">
303
345
  <%= t("pg_reports.ui.categories.requires_pg_stat") %>
304
346
  </div>
347
+ <% elsif target_disabled_reason %>
348
+ <div class="category-warning">
349
+ <%= target_disabled_reason %>
350
+ </div>
305
351
  <% end %>
306
352
  <div class="category-header">
307
353
  <div class="category-icon" style="background: <%= category[:color] %>20; color: <%= category[:color] %>;">
@@ -313,7 +359,7 @@ pg_stat_statements.track = all</pre>
313
359
 
314
360
  <div class="reports-list">
315
361
  <% category[:reports].each do |report_key, report| %>
316
- <% if category_key == :queries && !@pg_stat_status[:ready] %>
362
+ <% if is_disabled %>
317
363
  <div class="report-link disabled">
318
364
  <div class="report-link-info">
319
365
  <span class="report-link-name">
@@ -342,7 +388,37 @@ pg_stat_statements.track = all</pre>
342
388
  <% end %>
343
389
  </div>
344
390
 
391
+ <footer class="dashboard-footer">
392
+ <a href="https://github.com/deadalice/pg_reports" target="_blank" rel="noopener noreferrer">github.com/deadalice/pg_reports</a>
393
+ <span class="footer-sep">·</span>
394
+ <a href="mailto:deadalice@gmail.com">deadalice@gmail.com</a>
395
+ </footer>
396
+
345
397
  <style>
398
+ .dashboard-footer {
399
+ margin-top: 2rem;
400
+ padding-top: 1.25rem;
401
+ border-top: 1px solid var(--border-color);
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: center;
405
+ gap: 0.6rem;
406
+ flex-wrap: wrap;
407
+ color: var(--text-muted);
408
+ font-size: 0.8rem;
409
+ }
410
+ .dashboard-footer a {
411
+ color: var(--text-secondary);
412
+ text-decoration: none;
413
+ }
414
+ .dashboard-footer a:hover {
415
+ color: var(--accent-purple);
416
+ text-decoration: underline;
417
+ }
418
+ .dashboard-footer .footer-sep {
419
+ opacity: 0.5;
420
+ }
421
+
346
422
  .header-actions {
347
423
  display: flex;
348
424
  align-items: center;
@@ -354,15 +430,26 @@ pg_stat_statements.track = all</pre>
354
430
  font-size: 0.8rem;
355
431
  }
356
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
+
357
441
  .btn-info {
358
- width: 24px;
359
- height: 24px;
442
+ width: 2.125rem;
443
+ height: 2.125rem;
360
444
  padding: 0;
445
+ display: inline-flex;
446
+ align-items: center;
447
+ justify-content: center;
361
448
  background: var(--bg-tertiary);
362
449
  color: var(--text-muted);
363
450
  border: 1px solid var(--border-color);
364
- border-radius: 50%;
365
- font-size: 0.8rem;
451
+ border-radius: 4px;
452
+ font-size: 0.9rem;
366
453
  font-weight: 600;
367
454
  cursor: pointer;
368
455
  transition: all 0.15s;
@@ -410,7 +497,7 @@ pg_stat_statements.track = all</pre>
410
497
 
411
498
  .category-warning {
412
499
  padding: 0.625rem 1rem;
413
- margin: -1.5rem -1.5rem 1rem -1.5rem;
500
+ margin: -1rem -1rem 1rem -1rem;
414
501
  background: rgba(245, 158, 11, 0.1);
415
502
  border-bottom: 1px solid rgba(245, 158, 11, 0.2);
416
503
  border-radius: 6px 6px 0 0;
@@ -613,22 +700,6 @@ pg_stat_statements.track = all</pre>
613
700
  font-size: 1rem;
614
701
  }
615
702
 
616
- .database-badge {
617
- margin-left: 0.5rem;
618
- padding: 0.375rem 0.75rem;
619
- background: var(--bg-tertiary);
620
- border: 1px solid var(--border-color);
621
- border-radius: 8px;
622
- font-size: 0.75rem;
623
- font-weight: 500;
624
- color: var(--text-secondary);
625
- }
626
-
627
- .database-badge strong {
628
- color: var(--text-primary);
629
- font-weight: 600;
630
- }
631
-
632
703
  .live-indicator {
633
704
  width: 8px;
634
705
  height: 8px;
@@ -852,6 +923,18 @@ pg_stat_statements.track = all</pre>
852
923
  if (e.target === this) closePgStatModal();
853
924
  });
854
925
 
926
+ function showCreateExtensionModal() {
927
+ document.getElementById('create-extension-modal').style.display = 'flex';
928
+ }
929
+
930
+ function closeCreateExtensionModal() {
931
+ document.getElementById('create-extension-modal').style.display = 'none';
932
+ }
933
+
934
+ document.getElementById('create-extension-modal')?.addEventListener('click', function(e) {
935
+ if (e.target === this) closeCreateExtensionModal();
936
+ });
937
+
855
938
  function showResetConfirmModal() {
856
939
  document.getElementById('reset-confirm-modal').style.display = 'flex';
857
940
  }
@@ -918,6 +1001,7 @@ pg_stat_statements.track = all</pre>
918
1001
  } else {
919
1002
  showToast(data.message, 'error');
920
1003
  if (data.requires_restart) {
1004
+ closeCreateExtensionModal();
921
1005
  showPgStatInfo();
922
1006
  }
923
1007
  button.disabled = false;
@@ -990,13 +1074,12 @@ pg_stat_statements.track = all</pre>
990
1074
  if (!panel) return;
991
1075
 
992
1076
  panel.style.display = 'block';
993
- panel.innerHTML = `
994
- <div class="live-monitoring-header">
995
- <div class="live-monitoring-title">
996
- <span class="badge-dot error"></span>
997
- <span>${PG_REPORTS_I18N.status.monitoring_unavailable}</span>
998
- </div>
999
- </div>
1077
+
1078
+ // Replace only the metrics grid; preserve the header (which holds the
1079
+ // database selector) so the user can still switch databases when metrics
1080
+ // are unavailable on the current one.
1081
+ const grid = panel.querySelector('.live-metrics-grid');
1082
+ const fallbackHTML = `
1000
1083
  <div style="padding: 2rem; text-align: center; color: var(--text-muted);">
1001
1084
  <p style="margin-bottom: 1rem;">${PG_REPORTS_I18N.errors.unable_fetch_metrics}</p>
1002
1085
  <p style="font-size: 0.875rem;">${PG_REPORTS_I18N.errors.possible_causes}</p>
@@ -1010,6 +1093,16 @@ pg_stat_statements.track = all</pre>
1010
1093
  </button>
1011
1094
  </div>
1012
1095
  `;
1096
+
1097
+ if (grid) {
1098
+ grid.outerHTML = fallbackHTML;
1099
+ } else {
1100
+ panel.insertAdjacentHTML('beforeend', fallbackHTML);
1101
+ }
1102
+
1103
+ // Surface the unavailable status next to the live indicator.
1104
+ const indicator = panel.querySelector('.live-indicator');
1105
+ if (indicator) indicator.classList.add('error');
1013
1106
  }
1014
1107
 
1015
1108
  function startPolling() {
@@ -7,6 +7,10 @@
7
7
  <span><%= @categories[@category][:name] %></span>
8
8
  <span>/</span>
9
9
  <span><%= @report_info[:name] %></span>
10
+
11
+ <span class="breadcrumb-spacer"></span>
12
+ <%= render "target_selector" %>
13
+ <%= render "database_selector" %>
10
14
  </nav>
11
15
 
12
16
  <div class="report-header">