pg_reports 0.8.0 → 0.8.2

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 {
@@ -634,6 +654,7 @@
634
654
  }
635
655
 
636
656
  .query-monitoring-scope {
657
+ position: relative;
637
658
  font-weight: 400;
638
659
  font-size: 0.75rem;
639
660
  color: var(--text-muted);
@@ -643,6 +664,40 @@
643
664
  cursor: help;
644
665
  }
645
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
+
646
701
  .monitoring-indicator {
647
702
  width: 7px;
648
703
  height: 7px;
@@ -1000,6 +1055,10 @@
1000
1055
  max-width: 360px;
1001
1056
  }
1002
1057
 
1058
+ .modal-medium {
1059
+ max-width: 560px;
1060
+ }
1061
+
1003
1062
  /* Database selector — used inside .live-monitoring-title and .breadcrumb.
1004
1063
  Sizes to fit its longest option (no artificial cap), capped at the
1005
1064
  parent's available width so it never overruns neighboring controls. */
@@ -1128,7 +1187,10 @@
1128
1187
  <div id="ide-menu" class="ide-menu"></div>
1129
1188
 
1130
1189
  <script>
1131
- 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(/\/+$/, '');
1132
1194
  window.PG_REPORTS_I18N = <%= raw I18n.t("pg_reports.ui").to_json %>;
1133
1195
 
1134
1196
  // Format strings with %{var} placeholders
@@ -1137,6 +1199,26 @@
1137
1199
  return template.replace(/%\{(\w+)\}/g, function(_, k) { return vars[k] != null ? vars[k] : ''; });
1138
1200
  };
1139
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
+
1140
1222
  function showToast(message, type = 'success') {
1141
1223
  const toast = document.getElementById('toast');
1142
1224
  toast.textContent = message;
@@ -23,32 +23,41 @@
23
23
  </div>
24
24
 
25
25
  <div class="header-actions">
26
+ <button class="btn btn-small btn-primary" onclick="showRunQueryModal()" id="run-query-open-btn">
27
+ <%= t("pg_reports.ui.actions.run_query") %>
28
+ </button>
26
29
  <% if @pg_stat_status[:ready] %>
27
30
  <button class="btn btn-small btn-muted" onclick="showResetConfirmModal()" id="reset-btn">
28
31
  <%= t("pg_reports.ui.actions.reset_statistics") %>
29
32
  </button>
30
- <% else %>
31
- <button class="btn btn-small btn-primary" onclick="enablePgStatStatements(this)" id="enable-btn">
32
- <%= t("pg_reports.ui.actions.create_extension") %>
33
- </button>
34
- <button class="btn-info" onclick="showPgStatInfo()">?</button>
35
33
  <% end %>
36
34
  <button class="btn-info" onclick="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>">⚙️</button>
37
- <div class="header-badge" id="pg-stat-badge">
38
- <% if @pg_stat_status[:ready] %>
35
+
36
+ <% if !@pg_stat_status[:connected] %>
37
+ <div class="header-badge" id="pg-stat-badge">
38
+ <span class="badge-dot error"></span>
39
+ <span><%= t("pg_reports.ui.status.disconnected") %></span>
40
+ </div>
41
+ <% elsif @pg_stat_status[:ready] %>
42
+ <div class="header-badge" id="pg-stat-badge">
39
43
  <span class="badge-dot"></span>
40
44
  <span><%= t("pg_reports.ui.status.pg_stat_ready") %></span>
41
- <% elsif @pg_stat_status[:extension_installed] %>
45
+ </div>
46
+ <% elsif @pg_stat_status[:extension_installed] %>
47
+ <button type="button" class="header-badge header-badge-clickable" id="pg-stat-badge"
48
+ onclick="showPgStatInfo()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
42
49
  <span class="badge-dot warning"></span>
43
- <span><%= t("pg_reports.ui.status.extension_installed") %></span>
44
- <% elsif @pg_stat_status[:preloaded] %>
50
+ <span><%= t("pg_reports.ui.status.not_preloaded") %></span>
51
+ <span class="badge-info-icon" aria-hidden="true">ⓘ</span>
52
+ </button>
53
+ <% else %>
54
+ <button type="button" class="header-badge header-badge-clickable" id="pg-stat-badge"
55
+ onclick="showCreateExtensionModal()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
45
56
  <span class="badge-dot warning"></span>
46
- <span><%= t("pg_reports.ui.status.preloaded") %></span>
47
- <% else %>
48
- <span class="badge-dot error"></span>
49
- <span><%= t("pg_reports.ui.status.not_configured") %></span>
50
- <% end %>
51
- </div>
57
+ <span><%= t("pg_reports.ui.status.extension_missing") %></span>
58
+ <span class="badge-info-icon" aria-hidden="true">ⓘ</span>
59
+ </button>
60
+ <% end %>
52
61
  </div>
53
62
  </header>
54
63
 
@@ -81,6 +90,25 @@ pg_stat_statements.track = all</pre>
81
90
  </div>
82
91
  </div>
83
92
 
93
+ <!-- Create extension modal -->
94
+ <div id="create-extension-modal" class="modal" style="display: none;">
95
+ <div class="modal-content modal-medium">
96
+ <div class="modal-header">
97
+ <h3><%= t("pg_reports.ui.modals.create_extension_title") %></h3>
98
+ <button class="modal-close" onclick="closeCreateExtensionModal()">×</button>
99
+ </div>
100
+ <div class="modal-body">
101
+ <p><%= t("pg_reports.ui.modals.create_extension_intro") %></p>
102
+ <pre>CREATE EXTENSION IF NOT EXISTS pg_stat_statements;</pre>
103
+ <p class="warning-subtext"><%= t("pg_reports.ui.modals.create_extension_note") %></p>
104
+ <div class="modal-actions">
105
+ <button class="btn btn-secondary" onclick="closeCreateExtensionModal()"><%= t("pg_reports.ui.actions.cancel") %></button>
106
+ <button class="btn btn-primary" onclick="enablePgStatStatements(this)" id="modal-create-extension-btn"><%= t("pg_reports.ui.actions.create_extension") %></button>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
84
112
  <!-- Reset statistics confirmation modal -->
85
113
  <div id="reset-confirm-modal" class="modal" style="display: none;">
86
114
  <div class="modal-content modal-small">
@@ -142,6 +170,42 @@ pg_stat_statements.track = all</pre>
142
170
  </div>
143
171
  </div>
144
172
 
173
+ <!-- Run Query Modal -->
174
+ <div id="run-query-modal" class="modal" style="display: none;">
175
+ <div class="modal-content modal-run-query">
176
+ <div class="modal-header">
177
+ <h3><%= t("pg_reports.ui.modals.run_query_title") %></h3>
178
+ <button class="modal-close" onclick="closeRunQueryModal()">&times;</button>
179
+ </div>
180
+ <div class="modal-body">
181
+ <% if PgReports.config.allow_raw_query_execution %>
182
+ <label class="explain-label" for="run-query-input"><%= t("pg_reports.ui.modals.query_label") %></label>
183
+ <textarea id="run-query-input" class="run-query-textarea" spellcheck="false" placeholder="<%= t("pg_reports.ui.modals.run_query_placeholder") %>"></textarea>
184
+ <div class="run-query-actions">
185
+ <button class="btn btn-secondary" onclick="clearRunQuery()"><%= t("pg_reports.ui.actions.clear_all") %></button>
186
+ <button class="btn btn-primary" onclick="executeRunQuery()" id="btn-run-query"><%= t("pg_reports.ui.actions.run_query") %></button>
187
+ </div>
188
+ <div id="run-query-loading" class="loading" style="display: none;">
189
+ <div class="spinner"></div>
190
+ </div>
191
+ <div id="run-query-content"></div>
192
+ <% else %>
193
+ <div class="error-message">
194
+ <strong><%= t("pg_reports.ui.modals.query_execution_disabled_title") %></strong><br><br>
195
+ <%= t("pg_reports.ui.modals.query_execution_disabled_intro") %><br>
196
+ <code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
197
+ PgReports.configure do |config|<br>
198
+ &nbsp;&nbsp;config.allow_raw_query_execution = true<br>
199
+ end
200
+ </code>
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>
203
+ </div>
204
+ <% end %>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
145
209
  <!-- Live Monitoring Panel -->
146
210
  <div id="live-monitoring" class="live-monitoring-panel" style="display: none;">
147
211
  <div class="live-monitoring-header">
@@ -261,13 +325,17 @@ pg_stat_statements.track = all</pre>
261
325
  </div>
262
326
  </div>
263
327
 
264
- <!-- Query Monitoring Panel -->
328
+ <!-- Query Monitoring Panel: taps ActiveSupport::Notifications in the host
329
+ application's process. There is no host app in standalone mode, so the
330
+ panel is hidden entirely rather than showing a monitor with nothing to
331
+ monitor. -->
332
+ <% unless PgReports.config.standalone %>
265
333
  <div id="query-monitoring" class="query-monitoring-panel" style="display: none;">
266
334
  <div class="query-monitoring-header">
267
335
  <div class="query-monitoring-title">
268
336
  <span class="monitoring-indicator" id="monitor-indicator"></span>
269
337
  <span><%= t("pg_reports.ui.monitoring.query_monitor_title") %></span>
270
- <span class="query-monitoring-scope" title="<%= 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.") %>">
338
+ <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.") %>">
271
339
  <%= t("pg_reports.ui.monitoring.scope_note_short", default: "scope: host application") %>
272
340
  </span>
273
341
  <span class="session-badge" id="session-badge" style="display: none;">
@@ -306,6 +374,7 @@ pg_stat_statements.track = all</pre>
306
374
  </div>
307
375
  </div>
308
376
  </div>
377
+ <% end %>
309
378
 
310
379
  <div class="categories-grid">
311
380
  <% @categories.each do |category_key, category| %>
@@ -363,7 +432,37 @@ pg_stat_statements.track = all</pre>
363
432
  <% end %>
364
433
  </div>
365
434
 
435
+ <footer class="dashboard-footer">
436
+ <a href="https://github.com/deadalice/pg_reports" target="_blank" rel="noopener noreferrer">github.com/deadalice/pg_reports</a>
437
+ <span class="footer-sep">·</span>
438
+ <a href="mailto:deadalice@gmail.com">deadalice@gmail.com</a>
439
+ </footer>
440
+
366
441
  <style>
442
+ .dashboard-footer {
443
+ margin-top: 2rem;
444
+ padding-top: 1.25rem;
445
+ border-top: 1px solid var(--border-color);
446
+ display: flex;
447
+ align-items: center;
448
+ justify-content: center;
449
+ gap: 0.6rem;
450
+ flex-wrap: wrap;
451
+ color: var(--text-muted);
452
+ font-size: 0.8rem;
453
+ }
454
+ .dashboard-footer a {
455
+ color: var(--text-secondary);
456
+ text-decoration: none;
457
+ }
458
+ .dashboard-footer a:hover {
459
+ color: var(--accent-purple);
460
+ text-decoration: underline;
461
+ }
462
+ .dashboard-footer .footer-sep {
463
+ opacity: 0.5;
464
+ }
465
+
367
466
  .header-actions {
368
467
  display: flex;
369
468
  align-items: center;
@@ -375,15 +474,26 @@ pg_stat_statements.track = all</pre>
375
474
  font-size: 0.8rem;
376
475
  }
377
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
+
378
485
  .btn-info {
379
- width: 24px;
380
- height: 24px;
486
+ width: 2.125rem;
487
+ height: 2.125rem;
381
488
  padding: 0;
489
+ display: inline-flex;
490
+ align-items: center;
491
+ justify-content: center;
382
492
  background: var(--bg-tertiary);
383
493
  color: var(--text-muted);
384
494
  border: 1px solid var(--border-color);
385
- border-radius: 50%;
386
- font-size: 0.8rem;
495
+ border-radius: 4px;
496
+ font-size: 0.9rem;
387
497
  font-weight: 600;
388
498
  cursor: pointer;
389
499
  transition: all 0.15s;
@@ -431,7 +541,7 @@ pg_stat_statements.track = all</pre>
431
541
 
432
542
  .category-warning {
433
543
  padding: 0.625rem 1rem;
434
- margin: -1.5rem -1.5rem 1rem -1.5rem;
544
+ margin: -1rem -1rem 1rem -1rem;
435
545
  background: rgba(245, 158, 11, 0.1);
436
546
  border-bottom: 1px solid rgba(245, 158, 11, 0.2);
437
547
  border-radius: 6px 6px 0 0;
@@ -841,9 +951,254 @@ pg_stat_statements.track = all</pre>
841
951
  .toast.toast-warning {
842
952
  border-left: 3px solid var(--accent-amber);
843
953
  }
954
+
955
+ /* Run Query Modal */
956
+ .modal-run-query {
957
+ max-width: 1100px;
958
+ width: 95%;
959
+ }
960
+
961
+ .explain-label {
962
+ display: block;
963
+ font-size: 0.75rem;
964
+ font-weight: 600;
965
+ color: var(--text-muted);
966
+ text-transform: uppercase;
967
+ margin-bottom: 0.5rem;
968
+ }
969
+
970
+ .run-query-textarea {
971
+ width: 100%;
972
+ min-height: 180px;
973
+ padding: 1rem;
974
+ background: var(--bg-primary);
975
+ border: 1px solid var(--border-color);
976
+ border-radius: 8px;
977
+ color: var(--text-primary);
978
+ font-family: 'JetBrains Mono', monospace;
979
+ font-size: 0.85rem;
980
+ line-height: 1.5;
981
+ resize: vertical;
982
+ }
983
+
984
+ .run-query-textarea:focus {
985
+ outline: none;
986
+ border-color: var(--accent-blue);
987
+ box-shadow: 0 0 0 2px rgba(107, 159, 232, 0.2);
988
+ }
989
+
990
+ .run-query-textarea::placeholder {
991
+ color: var(--text-muted);
992
+ font-style: italic;
993
+ }
994
+
995
+ .run-query-actions {
996
+ display: flex;
997
+ gap: 0.75rem;
998
+ margin: 1rem 0;
999
+ }
1000
+
1001
+ .run-query-actions .btn {
1002
+ flex: 1;
1003
+ }
1004
+
1005
+ /* Query Results Table (mirrors _show_styles.html.erb — index.html.erb has
1006
+ its own <style> block and isn't loaded through that partial) */
1007
+ .query-results-wrapper {
1008
+ margin-top: 1rem;
1009
+ max-height: 400px;
1010
+ overflow: auto;
1011
+ border: 1px solid var(--border-color);
1012
+ border-radius: 8px;
1013
+ }
1014
+
1015
+ .query-results-table {
1016
+ width: 100%;
1017
+ border-collapse: collapse;
1018
+ font-family: 'JetBrains Mono', monospace;
1019
+ font-size: 0.75rem;
1020
+ }
1021
+
1022
+ .query-results-table th {
1023
+ position: sticky;
1024
+ top: 0;
1025
+ padding: 0.75rem;
1026
+ text-align: left;
1027
+ background: var(--bg-tertiary);
1028
+ color: var(--text-muted);
1029
+ font-weight: 500;
1030
+ text-transform: uppercase;
1031
+ font-size: 0.65rem;
1032
+ letter-spacing: 0.05em;
1033
+ border-bottom: 1px solid var(--border-color);
1034
+ }
1035
+
1036
+ .query-results-table td {
1037
+ padding: 0.5rem 0.75rem;
1038
+ border-bottom: 1px solid var(--border-color);
1039
+ color: var(--text-secondary);
1040
+ max-width: 300px;
1041
+ overflow: hidden;
1042
+ text-overflow: ellipsis;
1043
+ white-space: nowrap;
1044
+ }
1045
+
1046
+ .query-results-table tr:hover td {
1047
+ background: var(--bg-tertiary);
1048
+ color: var(--text-primary);
1049
+ }
1050
+
1051
+ .query-results-info {
1052
+ display: flex;
1053
+ align-items: center;
1054
+ justify-content: space-between;
1055
+ padding: 0.75rem 1rem;
1056
+ background: var(--bg-tertiary);
1057
+ border-radius: 8px;
1058
+ margin-top: 1rem;
1059
+ font-size: 0.8rem;
1060
+ color: var(--text-secondary);
1061
+ }
1062
+
1063
+ .query-results-info .count {
1064
+ color: var(--accent-blue);
1065
+ font-weight: 600;
1066
+ }
1067
+
1068
+ .query-results-info .time {
1069
+ color: var(--accent-green);
1070
+ font-weight: 600;
1071
+ }
844
1072
  </style>
845
1073
 
846
1074
  <script>
1075
+ const allowRawQueryExecution = <%= PgReports.config.allow_raw_query_execution.to_s.downcase %>;
1076
+
1077
+ function escapeHtmlAttr(text) {
1078
+ return String(text)
1079
+ .replace(/&/g, '&amp;')
1080
+ .replace(/"/g, '&quot;')
1081
+ .replace(/'/g, '&#39;')
1082
+ .replace(/</g, '&lt;')
1083
+ .replace(/>/g, '&gt;');
1084
+ }
1085
+
1086
+ function showRunQueryModal() {
1087
+ document.getElementById('run-query-modal').style.display = 'flex';
1088
+ document.getElementById('run-query-input')?.focus();
1089
+ }
1090
+
1091
+ function closeRunQueryModal() {
1092
+ document.getElementById('run-query-modal').style.display = 'none';
1093
+ }
1094
+
1095
+ document.getElementById('run-query-modal')?.addEventListener('click', function(e) {
1096
+ if (e.target === this) closeRunQueryModal();
1097
+ });
1098
+
1099
+ document.getElementById('run-query-input')?.addEventListener('keydown', function(e) {
1100
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
1101
+ e.preventDefault();
1102
+ executeRunQuery();
1103
+ }
1104
+ });
1105
+
1106
+ function clearRunQuery() {
1107
+ document.getElementById('run-query-input').value = '';
1108
+ document.getElementById('run-query-content').innerHTML = '';
1109
+ }
1110
+
1111
+ async function executeRunQuery() {
1112
+ const input = document.getElementById('run-query-input');
1113
+ const loading = document.getElementById('run-query-loading');
1114
+ const content = document.getElementById('run-query-content');
1115
+ const query = input.value.trim();
1116
+
1117
+ if (!query) {
1118
+ showToast(PG_REPORTS_I18N.errors.query_required, 'error');
1119
+ return;
1120
+ }
1121
+
1122
+ if (!allowRawQueryExecution) {
1123
+ showToast(PG_REPORTS_I18N.errors.execute_disabled_toast, 'error');
1124
+ content.innerHTML = `<div class="error-message">
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
+ &nbsp;&nbsp;config.allow_raw_query_execution = true<br>
1130
+ end
1131
+ </code>
1132
+ </div>`;
1133
+ return;
1134
+ }
1135
+
1136
+ loading.style.display = 'flex';
1137
+ content.innerHTML = '';
1138
+
1139
+ try {
1140
+ const response = await fetch(`${pgReportsRoot}/run_query`, {
1141
+ method: 'POST',
1142
+ headers: {
1143
+ 'Content-Type': 'application/json',
1144
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
1145
+ },
1146
+ body: JSON.stringify({ query: query })
1147
+ });
1148
+
1149
+ const data = await response.json();
1150
+ loading.style.display = 'none';
1151
+
1152
+ if (data.success) {
1153
+ let html = '';
1154
+
1155
+ html += `<div class="query-results-info">
1156
+ <span>${PG_REPORTS_I18N.results.rows_label} <span class="count">${data.count}</span></span>
1157
+ <span>${PG_REPORTS_I18N.results.execution_time_label} <span class="time">${data.execution_time} ms</span></span>
1158
+ </div>`;
1159
+
1160
+ if (data.rows && data.rows.length > 0) {
1161
+ html += '<div class="query-results-wrapper">';
1162
+ html += '<table class="query-results-table">';
1163
+
1164
+ html += '<thead><tr>';
1165
+ data.columns.forEach(col => {
1166
+ html += `<th>${escapeHtml(col)}</th>`;
1167
+ });
1168
+ html += '</tr></thead>';
1169
+
1170
+ html += '<tbody>';
1171
+ data.rows.forEach(row => {
1172
+ html += '<tr>';
1173
+ data.columns.forEach(col => {
1174
+ const value = row[col];
1175
+ const displayValue = value === null ? PG_REPORTS_I18N.results.null_placeholder : String(value);
1176
+ html += `<td title="${escapeHtmlAttr(displayValue)}">${escapeHtml(displayValue)}</td>`;
1177
+ });
1178
+ html += '</tr>';
1179
+ });
1180
+ html += '</tbody>';
1181
+
1182
+ html += '</table>';
1183
+ html += '</div>';
1184
+
1185
+ if (data.truncated) {
1186
+ 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>`;
1187
+ }
1188
+ } else {
1189
+ html += '<p style="margin-top: 1rem; color: var(--text-muted);">' + PG_REPORTS_I18N.results.no_rows_returned + '</p>';
1190
+ }
1191
+
1192
+ content.innerHTML = html;
1193
+ } else {
1194
+ content.innerHTML = `<div class="error-message">${escapeHtml(data.error || PG_REPORTS_I18N.errors.run_query_failed)}</div>`;
1195
+ }
1196
+ } catch (error) {
1197
+ loading.style.display = 'none';
1198
+ content.innerHTML = `<div class="error-message">${PG_REPORTS_I18N.errors.network_error_prefix} ${escapeHtml(error.message)}</div>`;
1199
+ }
1200
+ }
1201
+
847
1202
  function showPgStatInfo() {
848
1203
  document.getElementById('pg-stat-modal').style.display = 'flex';
849
1204
  }
@@ -857,6 +1212,18 @@ pg_stat_statements.track = all</pre>
857
1212
  if (e.target === this) closePgStatModal();
858
1213
  });
859
1214
 
1215
+ function showCreateExtensionModal() {
1216
+ document.getElementById('create-extension-modal').style.display = 'flex';
1217
+ }
1218
+
1219
+ function closeCreateExtensionModal() {
1220
+ document.getElementById('create-extension-modal').style.display = 'none';
1221
+ }
1222
+
1223
+ document.getElementById('create-extension-modal')?.addEventListener('click', function(e) {
1224
+ if (e.target === this) closeCreateExtensionModal();
1225
+ });
1226
+
860
1227
  function showResetConfirmModal() {
861
1228
  document.getElementById('reset-confirm-modal').style.display = 'flex';
862
1229
  }
@@ -923,6 +1290,7 @@ pg_stat_statements.track = all</pre>
923
1290
  } else {
924
1291
  showToast(data.message, 'error');
925
1292
  if (data.requires_restart) {
1293
+ closeCreateExtensionModal();
926
1294
  showPgStatInfo();
927
1295
  }
928
1296
  button.disabled = false;