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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +40 -70
- data/app/controllers/pg_reports/dashboard_controller.rb +185 -19
- data/app/views/layouts/pg_reports/application.html.erb +88 -6
- data/app/views/pg_reports/dashboard/index.html.erb +391 -23
- data/bin/pg_reports +111 -0
- data/config/brakeman.ignore +74 -0
- data/config/locales/en.yml +19 -5
- data/config/locales/ru.yml +19 -5
- data/config/locales/uk.yml +19 -5
- data/config/routes.rb +1 -0
- data/lib/pg_reports/configuration.rb +8 -0
- data/lib/pg_reports/connection/target.rb +1 -1
- data/lib/pg_reports/executor.rb +7 -2
- data/lib/pg_reports/grafana/exporter.rb +5 -1
- data/lib/pg_reports/modules/system.rb +28 -4
- data/lib/pg_reports/standalone.rb +210 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +9 -0
- metadata +7 -3
|
@@ -113,13 +113,32 @@
|
|
|
113
113
|
display: flex;
|
|
114
114
|
align-items: center;
|
|
115
115
|
gap: 0.4rem;
|
|
116
|
-
|
|
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:
|
|
310
|
-
color:
|
|
311
|
-
border-color:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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.
|
|
44
|
-
|
|
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.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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()">×</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
|
+
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"
|
|
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:
|
|
380
|
-
height:
|
|
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:
|
|
386
|
-
font-size: 0.
|
|
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: -
|
|
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, '&')
|
|
1080
|
+
.replace(/"/g, '"')
|
|
1081
|
+
.replace(/'/g, ''')
|
|
1082
|
+
.replace(/</g, '<')
|
|
1083
|
+
.replace(/>/g, '>');
|
|
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
|
+
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;
|