pg_reports 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +12 -8
- data/app/controllers/pg_reports/dashboard_controller.rb +277 -221
- data/app/controllers/pg_reports/metrics_controller.rb +13 -4
- data/app/views/layouts/pg_reports/application.html.erb +222 -93
- data/app/views/pg_reports/dashboard/_settings_modal.html.erb +55 -0
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +1 -42
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +75 -105
- data/app/views/pg_reports/dashboard/index.html.erb +346 -238
- data/app/views/pg_reports/dashboard/show.html.erb +1 -1
- data/bin/pg_reports +2 -2
- data/config/brakeman.ignore +10 -56
- data/config/locales/en.yml +45 -11
- data/config/locales/ru.yml +44 -11
- data/config/locales/uk.yml +44 -11
- data/config/routes.rb +2 -0
- data/lib/pg_reports/access_policy.rb +39 -0
- data/lib/pg_reports/compatibility.rb +10 -2
- data/lib/pg_reports/configuration.rb +33 -2
- data/lib/pg_reports/connection/error_translator.rb +21 -0
- data/lib/pg_reports/connection/target.rb +43 -10
- data/lib/pg_reports/csv_cell.rb +20 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +37 -30
- data/lib/pg_reports/definitions/connections/connection_stats.yml +4 -0
- data/lib/pg_reports/definitions/connections/idle_connections.yml +4 -0
- data/lib/pg_reports/definitions/connections/locks.yml +1 -0
- data/lib/pg_reports/definitions/connections/long_running_queries.yml +0 -1
- data/lib/pg_reports/definitions/indexes/bloated_indexes.yml +1 -2
- data/lib/pg_reports/definitions/indexes/index_correlation.yml +2 -1
- data/lib/pg_reports/definitions/indexes/missing_indexes.yml +1 -0
- data/lib/pg_reports/definitions/queries/frequent_queries.yml +50 -0
- data/lib/pg_reports/definitions/system/cache_stats.yml +2 -1
- data/lib/pg_reports/definitions/tables/bloated_tables.yml +1 -2
- data/lib/pg_reports/definitions/tables/recently_modified.yml +3 -3
- data/lib/pg_reports/definitions/tables/unused_tables.yml +1 -1
- data/lib/pg_reports/drop_index_migration.rb +56 -0
- data/lib/pg_reports/engine.rb +4 -3
- data/lib/pg_reports/executor.rb +46 -9
- data/lib/pg_reports/modules/queries.rb +1 -0
- data/lib/pg_reports/query_monitor.rb +478 -192
- data/lib/pg_reports/rate_limiter.rb +63 -0
- data/lib/pg_reports/report.rb +1 -1
- data/lib/pg_reports/report_definition.rb +11 -1
- data/lib/pg_reports/sql/connections/connection_stats.sql +2 -1
- data/lib/pg_reports/sql/connections/idle_connections.sql +1 -0
- data/lib/pg_reports/sql/connections/locks.sql +1 -0
- data/lib/pg_reports/sql/indexes/bloated_indexes.sql +16 -8
- data/lib/pg_reports/sql/queries/activity_sample.sql +22 -0
- data/lib/pg_reports/sql/queries/frequent_queries.sql +42 -0
- data/lib/pg_reports/sql_redactor.rb +32 -0
- data/lib/pg_reports/standalone.rb +83 -20
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +7 -2
- metadata +18 -9
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
<%= t("pg_reports.ui.actions.reset_statistics") %>
|
|
29
29
|
</button>
|
|
30
30
|
<% end %>
|
|
31
|
-
<button type="button" class="btn-icon" onclick="
|
|
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
32
|
<svg class="icon icon-lg"><use href="#i-settings"></use></svg>
|
|
33
33
|
</button>
|
|
34
34
|
|
|
@@ -127,47 +127,7 @@ pg_stat_statements.track = all</pre>
|
|
|
127
127
|
</div>
|
|
128
128
|
|
|
129
129
|
<!-- IDE Settings Modal -->
|
|
130
|
-
|
|
131
|
-
<div class="modal-content modal-small">
|
|
132
|
-
<div class="modal-header">
|
|
133
|
-
<h3><%= t("pg_reports.ui.modals.ide_settings_title") %></h3>
|
|
134
|
-
<button class="modal-close" onclick="closeIdeSettingsModal()">×</button>
|
|
135
|
-
</div>
|
|
136
|
-
<div class="modal-body">
|
|
137
|
-
<p class="settings-label"><%= t("pg_reports.ui.settings.default_ide_label") %></p>
|
|
138
|
-
<div class="ide-options">
|
|
139
|
-
<label class="ide-option">
|
|
140
|
-
<input type="radio" name="default-ide" value="" onchange="setDefaultIde('')">
|
|
141
|
-
<span><%= t("pg_reports.ui.settings.ide_show_menu") %></span>
|
|
142
|
-
</label>
|
|
143
|
-
<label class="ide-option">
|
|
144
|
-
<input type="radio" name="default-ide" value="vscode-wsl" onchange="setDefaultIde('vscode-wsl')">
|
|
145
|
-
<span><%= t("pg_reports.ui.settings.ide_vscode_wsl") %></span>
|
|
146
|
-
</label>
|
|
147
|
-
<label class="ide-option">
|
|
148
|
-
<input type="radio" name="default-ide" value="vscode" onchange="setDefaultIde('vscode')">
|
|
149
|
-
<span><%= t("pg_reports.ui.settings.ide_vscode") %></span>
|
|
150
|
-
</label>
|
|
151
|
-
<label class="ide-option">
|
|
152
|
-
<input type="radio" name="default-ide" value="rubymine" onchange="setDefaultIde('rubymine')">
|
|
153
|
-
<span><%= t("pg_reports.ui.settings.ide_rubymine") %></span>
|
|
154
|
-
</label>
|
|
155
|
-
<label class="ide-option">
|
|
156
|
-
<input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
|
|
157
|
-
<span><%= t("pg_reports.ui.settings.ide_intellij") %></span>
|
|
158
|
-
</label>
|
|
159
|
-
<label class="ide-option">
|
|
160
|
-
<input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
|
|
161
|
-
<span><%= t("pg_reports.ui.settings.ide_cursor_wsl") %></span>
|
|
162
|
-
</label>
|
|
163
|
-
<label class="ide-option">
|
|
164
|
-
<input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
|
|
165
|
-
<span><%= t("pg_reports.ui.settings.ide_cursor") %></span>
|
|
166
|
-
</label>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
130
|
+
<%= render "settings_modal" %>
|
|
171
131
|
|
|
172
132
|
<!-- Run Query Modal -->
|
|
173
133
|
<div id="run-query-modal" class="modal" style="display: none;">
|
|
@@ -239,7 +199,6 @@ end</pre>
|
|
|
239
199
|
<svg id="sparkline-connections" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
240
200
|
</div>
|
|
241
201
|
</div>
|
|
242
|
-
<div class="metric-status" id="status-connections"></div>
|
|
243
202
|
</div>
|
|
244
203
|
|
|
245
204
|
<!-- TPS -->
|
|
@@ -260,7 +219,6 @@ end</pre>
|
|
|
260
219
|
<svg id="sparkline-tps" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
261
220
|
</div>
|
|
262
221
|
</div>
|
|
263
|
-
<div class="metric-status" id="status-tps"></div>
|
|
264
222
|
</div>
|
|
265
223
|
|
|
266
224
|
<!-- Cache Hit Ratio -->
|
|
@@ -279,7 +237,6 @@ end</pre>
|
|
|
279
237
|
<svg id="sparkline-cache" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
280
238
|
</div>
|
|
281
239
|
</div>
|
|
282
|
-
<div class="metric-status" id="status-cache"></div>
|
|
283
240
|
</div>
|
|
284
241
|
|
|
285
242
|
<!-- Long Running Queries -->
|
|
@@ -298,7 +255,6 @@ end</pre>
|
|
|
298
255
|
<svg id="sparkline-longrunning" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
299
256
|
</div>
|
|
300
257
|
</div>
|
|
301
|
-
<div class="metric-status" id="status-longrunning"></div>
|
|
302
258
|
</div>
|
|
303
259
|
|
|
304
260
|
<!-- Blocked Queries -->
|
|
@@ -317,29 +273,32 @@ end</pre>
|
|
|
317
273
|
<svg id="sparkline-blocked" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
|
|
318
274
|
</div>
|
|
319
275
|
</div>
|
|
320
|
-
<div class="metric-status" id="status-blocked"></div>
|
|
321
276
|
</div>
|
|
322
277
|
</div>
|
|
323
278
|
</div>
|
|
324
279
|
|
|
325
|
-
<!-- Query Monitoring Panel:
|
|
326
|
-
application's
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<% unless PgReports.config.standalone %>
|
|
280
|
+
<!-- Query Monitoring Panel: captures from ActiveSupport::Notifications in the
|
|
281
|
+
host application's processes, or by sampling pg_stat_activity in standalone
|
|
282
|
+
mode, where there is no host application. -->
|
|
283
|
+
<% if PgReports.config.query_monitor_available? %>
|
|
330
284
|
<div id="query-monitoring" class="query-monitoring-panel" style="display: none;">
|
|
331
285
|
<div class="query-monitoring-header">
|
|
332
286
|
<div class="query-monitoring-title">
|
|
333
287
|
<span class="monitoring-indicator" id="monitor-indicator"></span>
|
|
334
288
|
<span><%= t("pg_reports.ui.monitoring.query_monitor_title") %></span>
|
|
335
|
-
|
|
336
|
-
|
|
289
|
+
<% scope_note = PgReports.config.standalone ? "sampled" : "host" %>
|
|
290
|
+
<span class="query-monitoring-scope" tabindex="0" data-tooltip="<%= t("pg_reports.ui.monitoring.scope_note_long_#{scope_note}") %>">
|
|
291
|
+
<%= t("pg_reports.ui.monitoring.scope_note_short_#{scope_note}") %>
|
|
337
292
|
</span>
|
|
338
293
|
<span class="session-badge" id="session-badge" style="display: none;">
|
|
339
|
-
<%= t("pg_reports.ui.monitoring.
|
|
294
|
+
<%= t("pg_reports.ui.monitoring.auto_stop_label") %> <strong id="monitor-countdown"></strong>
|
|
340
295
|
</span>
|
|
341
296
|
</div>
|
|
342
297
|
<div class="query-monitoring-controls">
|
|
298
|
+
<div class="query-source-switch" id="query-source-switch" role="group" aria-label="<%= t("pg_reports.ui.monitoring.source_switch_label") %>" hidden>
|
|
299
|
+
<button type="button" data-source="notifications" title="<%= t("pg_reports.ui.monitoring.source_notifications_title") %>" onclick="selectQueryMonitorSource('notifications')"><%= t("pg_reports.ui.monitoring.source_notifications") %></button>
|
|
300
|
+
<button type="button" data-source="activity" title="<%= t("pg_reports.ui.monitoring.source_activity_title") %>" onclick="selectQueryMonitorSource('activity')"><%= t("pg_reports.ui.monitoring.source_activity") %></button>
|
|
301
|
+
</div>
|
|
343
302
|
<button class="btn btn-primary" onclick="startQueryMonitoring(this)" id="start-monitor-btn">
|
|
344
303
|
<%= t("pg_reports.ui.actions.start_monitoring") %>
|
|
345
304
|
</button>
|
|
@@ -365,10 +324,28 @@ end</pre>
|
|
|
365
324
|
</div>
|
|
366
325
|
</div>
|
|
367
326
|
|
|
327
|
+
<div class="query-scope-warning" id="query-scope-warning" style="display: none;">
|
|
328
|
+
<%= t("pg_reports.ui.monitoring.scope_process_warning") %>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div class="query-scope-warning" id="query-visibility-warning" style="display: none;"></div>
|
|
332
|
+
<div class="query-scope-warning query-monitor-error" id="query-monitor-error" style="display: none;"></div>
|
|
333
|
+
|
|
368
334
|
<div class="query-feed" id="query-feed">
|
|
369
|
-
<div class="query-feed-empty">
|
|
335
|
+
<div class="query-feed-empty" id="query-feed-empty">
|
|
370
336
|
<%= t("pg_reports.ui.monitoring.feed_empty") %>
|
|
371
337
|
</div>
|
|
338
|
+
<table class="query-feed-table" id="query-feed-table" style="display: none;">
|
|
339
|
+
<thead>
|
|
340
|
+
<tr>
|
|
341
|
+
<th class="col-time"><%= t("pg_reports.ui.monitoring.col_time") %></th>
|
|
342
|
+
<th class="col-duration"><%= t("pg_reports.ui.monitoring.col_duration") %></th>
|
|
343
|
+
<th class="col-source"><%= t("pg_reports.ui.monitoring.col_source") %></th>
|
|
344
|
+
<th class="col-sql"><%= t("pg_reports.ui.monitoring.col_sql") %></th>
|
|
345
|
+
</tr>
|
|
346
|
+
</thead>
|
|
347
|
+
<tbody id="query-feed-body"></tbody>
|
|
348
|
+
</table>
|
|
372
349
|
</div>
|
|
373
350
|
</div>
|
|
374
351
|
<% end %>
|
|
@@ -654,11 +631,14 @@ end</pre>
|
|
|
654
631
|
value colour and the sparkline, never by a coloured edge: "healthy" is
|
|
655
632
|
the common case and should read as quiet, not decorated. */
|
|
656
633
|
.live-metric-card {
|
|
634
|
+
display: flex;
|
|
635
|
+
flex-direction: column;
|
|
657
636
|
background: var(--bg-tertiary);
|
|
658
637
|
border: 1px solid var(--border-color);
|
|
659
638
|
border-radius: var(--radius);
|
|
660
639
|
padding: 0.875rem 1rem;
|
|
661
|
-
|
|
640
|
+
/* The trend band below bleeds to the card's edges. */
|
|
641
|
+
overflow: hidden;
|
|
662
642
|
transition: border-color 0.12s ease;
|
|
663
643
|
}
|
|
664
644
|
|
|
@@ -674,7 +654,7 @@ end</pre>
|
|
|
674
654
|
display: flex;
|
|
675
655
|
align-items: center;
|
|
676
656
|
gap: 0.5rem;
|
|
677
|
-
margin-bottom: 0.
|
|
657
|
+
margin-bottom: 0.25rem;
|
|
678
658
|
}
|
|
679
659
|
|
|
680
660
|
.metric-icon {
|
|
@@ -693,6 +673,9 @@ end</pre>
|
|
|
693
673
|
.metric-body {
|
|
694
674
|
display: flex;
|
|
695
675
|
flex-direction: column;
|
|
676
|
+
/* Figures hug the right edge; the trend band below opts back out. */
|
|
677
|
+
align-items: flex-end;
|
|
678
|
+
flex: 1;
|
|
696
679
|
gap: 0.1875rem;
|
|
697
680
|
}
|
|
698
681
|
|
|
@@ -719,17 +702,27 @@ end</pre>
|
|
|
719
702
|
}
|
|
720
703
|
|
|
721
704
|
.metric-detail {
|
|
705
|
+
max-width: 100%;
|
|
722
706
|
font-size: 0.7rem;
|
|
723
707
|
color: var(--text-muted);
|
|
708
|
+
text-align: right;
|
|
724
709
|
white-space: nowrap;
|
|
725
710
|
overflow: hidden;
|
|
726
711
|
text-overflow: ellipsis;
|
|
727
712
|
}
|
|
728
713
|
|
|
729
|
-
/*
|
|
714
|
+
/* Trend band. Negative margins cancel the card's padding so it spans the full
|
|
715
|
+
width and sits flush with the bottom edge — as a floating inset line it left
|
|
716
|
+
the lower half of the card reading as dead space. `margin-top: auto` keeps
|
|
717
|
+
it pinned down when cards in a row differ in height. */
|
|
730
718
|
.metric-sparkline {
|
|
731
|
-
|
|
732
|
-
|
|
719
|
+
/* Full width, regardless of the right-alignment its siblings use. */
|
|
720
|
+
align-self: stretch;
|
|
721
|
+
height: 2.25rem;
|
|
722
|
+
/* auto keeps the band on the bottom edge when cards in a row differ in
|
|
723
|
+
height; the negative side/bottom margins cancel the card's padding. */
|
|
724
|
+
margin: auto -1rem -0.875rem;
|
|
725
|
+
padding-top: 0.5rem;
|
|
733
726
|
color: var(--text-muted);
|
|
734
727
|
}
|
|
735
728
|
|
|
@@ -742,20 +735,6 @@ end</pre>
|
|
|
742
735
|
display: block;
|
|
743
736
|
}
|
|
744
737
|
|
|
745
|
-
/* Status indicator */
|
|
746
|
-
.metric-status {
|
|
747
|
-
position: absolute;
|
|
748
|
-
top: 0.75rem;
|
|
749
|
-
right: 0.75rem;
|
|
750
|
-
width: 6px;
|
|
751
|
-
height: 6px;
|
|
752
|
-
border-radius: 50%;
|
|
753
|
-
background: var(--border-strong);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
.metric-status.warning { background: var(--accent-amber); }
|
|
757
|
-
.metric-status.critical { background: var(--accent-rose); }
|
|
758
|
-
|
|
759
738
|
/* Run Query Modal */
|
|
760
739
|
.modal-run-query {
|
|
761
740
|
max-width: 1100px;
|
|
@@ -1329,23 +1308,30 @@ end</pre>
|
|
|
1329
1308
|
|
|
1330
1309
|
const width = 100;
|
|
1331
1310
|
const height = 30;
|
|
1311
|
+
// No horizontal inset: the band spans the card's full width, so the trace
|
|
1312
|
+
// should reach both edges. Vertical inset only, to keep the stroke from
|
|
1313
|
+
// being clipped at the extremes.
|
|
1332
1314
|
const padding = 2;
|
|
1333
1315
|
|
|
1334
1316
|
const min = Math.min(...data);
|
|
1335
1317
|
const max = Math.max(...data);
|
|
1336
|
-
const range = max - min
|
|
1318
|
+
const range = max - min;
|
|
1337
1319
|
|
|
1338
1320
|
const points = data.map((value, index) => {
|
|
1339
|
-
const x =
|
|
1340
|
-
|
|
1321
|
+
const x = (index / (data.length - 1)) * width;
|
|
1322
|
+
// A series that never moves is drawn mid-band: pinned to the floor it
|
|
1323
|
+
// reads as "zero" (or as a stray underline) rather than "steady".
|
|
1324
|
+
const y = range === 0
|
|
1325
|
+
? height / 2
|
|
1326
|
+
: height - padding - ((value - min) / range) * (height - 2 * padding);
|
|
1341
1327
|
return { x, y };
|
|
1342
1328
|
});
|
|
1343
1329
|
|
|
1344
1330
|
const pathD = points.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`)).join(' ');
|
|
1345
1331
|
|
|
1346
1332
|
const areaD = pathD +
|
|
1347
|
-
` L ${points[points.length - 1].x} ${height
|
|
1348
|
-
` L ${points[0].x} ${height
|
|
1333
|
+
` L ${points[points.length - 1].x} ${height}` +
|
|
1334
|
+
` L ${points[0].x} ${height} Z`;
|
|
1349
1335
|
|
|
1350
1336
|
const area = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1351
1337
|
area.setAttribute('d', areaD);
|
|
@@ -1366,8 +1352,7 @@ end</pre>
|
|
|
1366
1352
|
|
|
1367
1353
|
function updateStatus(metric, value, badDirection) {
|
|
1368
1354
|
const card = document.querySelector(`[data-metric="${metric}"]`);
|
|
1369
|
-
|
|
1370
|
-
if (!card || !statusDot) return;
|
|
1355
|
+
if (!card) return;
|
|
1371
1356
|
|
|
1372
1357
|
const thresholds = LIVE_CONFIG.thresholds[metric];
|
|
1373
1358
|
let status = 'ok';
|
|
@@ -1384,8 +1369,6 @@ end</pre>
|
|
|
1384
1369
|
|
|
1385
1370
|
card.classList.remove('status-ok', 'status-warning', 'status-critical');
|
|
1386
1371
|
card.classList.add(`status-${status}`);
|
|
1387
|
-
statusDot.classList.remove('ok', 'warning', 'critical');
|
|
1388
|
-
statusDot.classList.add(status);
|
|
1389
1372
|
}
|
|
1390
1373
|
|
|
1391
1374
|
document.addEventListener('DOMContentLoaded', initLiveMonitoring);
|
|
@@ -1407,10 +1390,6 @@ end</pre>
|
|
|
1407
1390
|
'cursor': 5
|
|
1408
1391
|
};
|
|
1409
1392
|
|
|
1410
|
-
function getDefaultIde() {
|
|
1411
|
-
return localStorage.getItem('pgReportsDefaultIde') || '';
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
1393
|
function generateIdeUrls(filePath, lineNumber) {
|
|
1415
1394
|
if (!filePath) return [];
|
|
1416
1395
|
|
|
@@ -1423,40 +1402,45 @@ end</pre>
|
|
|
1423
1402
|
const line = lineNumber || 1;
|
|
1424
1403
|
const urls = [];
|
|
1425
1404
|
|
|
1405
|
+
// The path can come from a SQL comment, which anyone with database access
|
|
1406
|
+
// can write, so it is encoded here and never reaches a URL as it was.
|
|
1407
|
+
const urlPath = absolutePath.split('/').map(encodeURIComponent).join('/');
|
|
1408
|
+
const urlParam = encodeURIComponent(absolutePath);
|
|
1409
|
+
|
|
1426
1410
|
// VSCode (WSL Remote format)
|
|
1427
1411
|
urls.push({
|
|
1428
1412
|
name: PG_REPORTS_I18N.settings.ide_vscode_wsl,
|
|
1429
|
-
url: `vscode://vscode-remote/wsl+${wslDistro}${
|
|
1413
|
+
url: `vscode://vscode-remote/wsl+${wslDistro}${urlPath}:${line}`
|
|
1430
1414
|
});
|
|
1431
1415
|
|
|
1432
1416
|
// VSCode (direct path)
|
|
1433
1417
|
urls.push({
|
|
1434
1418
|
name: PG_REPORTS_I18N.settings.ide_vscode,
|
|
1435
|
-
url: `vscode://file${
|
|
1419
|
+
url: `vscode://file${urlPath}:${line}`
|
|
1436
1420
|
});
|
|
1437
1421
|
|
|
1438
1422
|
// RubyMine
|
|
1439
1423
|
urls.push({
|
|
1440
1424
|
name: PG_REPORTS_I18N.settings.ide_rubymine,
|
|
1441
|
-
url: `rubymine://open?file=${
|
|
1425
|
+
url: `rubymine://open?file=${urlParam}&line=${line}`
|
|
1442
1426
|
});
|
|
1443
1427
|
|
|
1444
1428
|
// IntelliJ
|
|
1445
1429
|
urls.push({
|
|
1446
1430
|
name: PG_REPORTS_I18N.settings.ide_intellij,
|
|
1447
|
-
url: `idea://open?file=${
|
|
1431
|
+
url: `idea://open?file=${urlParam}&line=${line}`
|
|
1448
1432
|
});
|
|
1449
1433
|
|
|
1450
1434
|
// Cursor (WSL Remote format)
|
|
1451
1435
|
urls.push({
|
|
1452
1436
|
name: PG_REPORTS_I18N.settings.ide_cursor_wsl,
|
|
1453
|
-
url: `cursor://vscode-remote/wsl+${wslDistro}${
|
|
1437
|
+
url: `cursor://vscode-remote/wsl+${wslDistro}${urlPath}:${line}`
|
|
1454
1438
|
});
|
|
1455
1439
|
|
|
1456
1440
|
// Cursor (direct path)
|
|
1457
1441
|
urls.push({
|
|
1458
1442
|
name: PG_REPORTS_I18N.settings.ide_cursor,
|
|
1459
|
-
url: `cursor://file${
|
|
1443
|
+
url: `cursor://file${urlPath}:${line}`
|
|
1460
1444
|
});
|
|
1461
1445
|
|
|
1462
1446
|
return urls;
|
|
@@ -1486,11 +1470,13 @@ end</pre>
|
|
|
1486
1470
|
const menu = document.getElementById('ide-menu');
|
|
1487
1471
|
const overlay = document.getElementById('ide-dropdown-overlay');
|
|
1488
1472
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1473
|
+
menu.replaceChildren(...ideUrls.map(ide => {
|
|
1474
|
+
const link = document.createElement('a');
|
|
1475
|
+
link.href = ide.url;
|
|
1476
|
+
link.textContent = ide.name;
|
|
1477
|
+
link.addEventListener('click', closeIdeMenu);
|
|
1478
|
+
return link;
|
|
1479
|
+
}));
|
|
1494
1480
|
|
|
1495
1481
|
// Position menu near the clicked element
|
|
1496
1482
|
if (event && event.target) {
|
|
@@ -1509,80 +1495,130 @@ end</pre>
|
|
|
1509
1495
|
document.getElementById('ide-dropdown-overlay')?.classList.remove('show');
|
|
1510
1496
|
}
|
|
1511
1497
|
|
|
1512
|
-
function showIdeSettingsModal() {
|
|
1513
|
-
const modal = document.getElementById('ide-settings-modal');
|
|
1514
|
-
modal.style.display = 'flex';
|
|
1515
|
-
loadIdeSettingsState();
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
function closeIdeSettingsModal() {
|
|
1519
|
-
document.getElementById('ide-settings-modal').style.display = 'none';
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
function setDefaultIde(ideKey) {
|
|
1523
|
-
if (ideKey) {
|
|
1524
|
-
localStorage.setItem('pgReportsDefaultIde', ideKey);
|
|
1525
|
-
} else {
|
|
1526
|
-
localStorage.removeItem('pgReportsDefaultIde');
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function loadIdeSettingsState() {
|
|
1531
|
-
const currentIde = getDefaultIde();
|
|
1532
|
-
const radios = document.querySelectorAll('input[name="default-ide"]');
|
|
1533
|
-
radios.forEach(radio => {
|
|
1534
|
-
radio.checked = (radio.value === currentIde);
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
// Close IDE settings modal on backdrop click
|
|
1539
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
1540
|
-
document.getElementById('ide-settings-modal')?.addEventListener('click', function(e) {
|
|
1541
|
-
if (e.target === this) {
|
|
1542
|
-
closeIdeSettingsModal();
|
|
1543
|
-
}
|
|
1544
|
-
});
|
|
1545
|
-
});
|
|
1546
|
-
|
|
1547
1498
|
// ============================================
|
|
1548
1499
|
// Query Monitoring
|
|
1549
1500
|
// ============================================
|
|
1550
1501
|
|
|
1551
1502
|
const QUERY_MONITOR_CONFIG = {
|
|
1552
1503
|
pollInterval: 2000,
|
|
1553
|
-
|
|
1504
|
+
// Reaching every process takes one flush, so a stopped session settles shortly after
|
|
1505
|
+
settleDelay: 1200,
|
|
1506
|
+
maxQueries: 100
|
|
1554
1507
|
};
|
|
1555
1508
|
|
|
1556
1509
|
let queryMonitorEnabled = false;
|
|
1557
1510
|
let queryMonitorPollTimer = null;
|
|
1511
|
+
let queryMonitorCountdownTimer = null;
|
|
1512
|
+
let queryMonitorExpiresAt = null;
|
|
1558
1513
|
let currentSessionId = null;
|
|
1559
1514
|
let queryCount = 0;
|
|
1515
|
+
let queryLastSeq = 0;
|
|
1516
|
+
let queryMonitorSource = null;
|
|
1517
|
+
const queryFeedData = new WeakMap();
|
|
1518
|
+
|
|
1519
|
+
function readSavedQueryMonitorSource() {
|
|
1520
|
+
try {
|
|
1521
|
+
return localStorage.getItem('pgReportsMonitorSource');
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function selectQueryMonitorSource(source) {
|
|
1528
|
+
queryMonitorSource = source;
|
|
1529
|
+
try {
|
|
1530
|
+
localStorage.setItem('pgReportsMonitorSource', source);
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
// Only the remembered choice is lost
|
|
1533
|
+
}
|
|
1534
|
+
renderQueryMonitorSource();
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function renderQueryMonitorSource() {
|
|
1538
|
+
const chip = document.querySelector('.query-monitoring-scope');
|
|
1539
|
+
const key = queryMonitorSource === 'activity' ? 'sampled' : 'host';
|
|
1540
|
+
chip.textContent = PG_REPORTS_I18N.monitoring[`scope_note_short_${key}`];
|
|
1541
|
+
chip.dataset.tooltip = PG_REPORTS_I18N.monitoring[`scope_note_long_${key}`];
|
|
1542
|
+
|
|
1543
|
+
document.querySelectorAll('#query-source-switch button').forEach(button => {
|
|
1544
|
+
button.classList.toggle('active', button.dataset.source === queryMonitorSource);
|
|
1545
|
+
button.disabled = queryMonitorEnabled;
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function applyQueryMonitorSources(data) {
|
|
1550
|
+
const available = data.available_sources || [];
|
|
1551
|
+
document.getElementById('query-source-switch').hidden = available.length < 2;
|
|
1552
|
+
|
|
1553
|
+
const saved = readSavedQueryMonitorSource();
|
|
1554
|
+
// A running session decides; otherwise the viewer's last choice, if still on offer
|
|
1555
|
+
queryMonitorSource = data.enabled || !available.includes(saved) ? data.source : saved;
|
|
1556
|
+
renderQueryMonitorSource();
|
|
1557
|
+
}
|
|
1560
1558
|
|
|
1561
1559
|
function initQueryMonitoring() {
|
|
1562
1560
|
const panel = document.getElementById('query-monitoring');
|
|
1563
1561
|
if (!panel) return;
|
|
1564
1562
|
|
|
1563
|
+
document.getElementById('query-feed-body').addEventListener('click', handleQueryFeedClick);
|
|
1564
|
+
|
|
1565
1565
|
// Check initial status
|
|
1566
1566
|
checkQueryMonitorStatus();
|
|
1567
1567
|
}
|
|
1568
1568
|
|
|
1569
|
+
function applyQueryMonitorStatus(data) {
|
|
1570
|
+
if (data.max_queries) QUERY_MONITOR_CONFIG.maxQueries = data.max_queries;
|
|
1571
|
+
if (typeof data.query_count === 'number') queryCount = data.query_count;
|
|
1572
|
+
|
|
1573
|
+
document.getElementById('query-count').textContent = queryCount;
|
|
1574
|
+
document.getElementById('query-scope-warning').style.display = data.scope === 'process' ? 'block' : 'none';
|
|
1575
|
+
|
|
1576
|
+
const visibilityWarning = document.getElementById('query-visibility-warning');
|
|
1577
|
+
visibilityWarning.style.display = data.hidden_sessions > 0 ? 'block' : 'none';
|
|
1578
|
+
visibilityWarning.textContent = pgReportsFormat(PG_REPORTS_I18N.monitoring.hidden_sessions_warning, { count: data.hidden_sessions });
|
|
1579
|
+
|
|
1580
|
+
queryMonitorExpiresAt = data.enabled && typeof data.expires_in === 'number'
|
|
1581
|
+
? Date.now() + data.expires_in * 1000
|
|
1582
|
+
: null;
|
|
1583
|
+
renderQueryMonitorCountdown();
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function renderQueryMonitorError(message) {
|
|
1587
|
+
const element = document.getElementById('query-monitor-error');
|
|
1588
|
+
element.style.display = message ? 'block' : 'none';
|
|
1589
|
+
element.textContent = message ? `${PG_REPORTS_I18N.errors.query_monitoring_error}: ${message}` : '';
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function renderQueryMonitorCountdown() {
|
|
1593
|
+
const element = document.getElementById('monitor-countdown');
|
|
1594
|
+
if (!element || queryMonitorExpiresAt === null) return;
|
|
1595
|
+
|
|
1596
|
+
const seconds = Math.max(0, Math.ceil((queryMonitorExpiresAt - Date.now()) / 1000));
|
|
1597
|
+
element.textContent = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1569
1600
|
async function checkQueryMonitorStatus() {
|
|
1570
1601
|
try {
|
|
1571
1602
|
const response = await fetch(`${pgReportsRoot}/query_monitor/status`);
|
|
1572
1603
|
const data = await response.json();
|
|
1573
1604
|
|
|
1574
1605
|
queryHistoryAvailable = data.history_available === true;
|
|
1606
|
+
applyQueryMonitorStatus(data);
|
|
1607
|
+
renderQueryMonitorError(data.success ? data.capture_error : data.error);
|
|
1608
|
+
queryMonitorEnabled = data.success && data.enabled;
|
|
1609
|
+
applyQueryMonitorSources(data);
|
|
1575
1610
|
|
|
1576
1611
|
if (data.success && data.enabled) {
|
|
1577
1612
|
queryMonitorEnabled = true;
|
|
1578
1613
|
currentSessionId = data.session_id;
|
|
1579
|
-
queryCount = data.query_count;
|
|
1580
1614
|
updateQueryMonitorUI(true);
|
|
1581
1615
|
startQueryMonitorPolling();
|
|
1582
1616
|
} else {
|
|
1583
1617
|
// Monitoring not active
|
|
1584
1618
|
queryMonitorEnabled = false;
|
|
1585
1619
|
updateQueryMonitorUI(false);
|
|
1620
|
+
// The last session's queries outlive it, so a reload does not lose them
|
|
1621
|
+
if (queryCount > 0) fetchQueryFeed();
|
|
1586
1622
|
}
|
|
1587
1623
|
} catch (error) {
|
|
1588
1624
|
console.error('Failed to check query monitor status:', error);
|
|
@@ -1638,7 +1674,8 @@ end</pre>
|
|
|
1638
1674
|
headers: {
|
|
1639
1675
|
'Content-Type': 'application/json',
|
|
1640
1676
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
1641
|
-
}
|
|
1677
|
+
},
|
|
1678
|
+
body: JSON.stringify({ source: queryMonitorSource })
|
|
1642
1679
|
});
|
|
1643
1680
|
|
|
1644
1681
|
const data = await response.json();
|
|
@@ -1646,11 +1683,13 @@ end</pre>
|
|
|
1646
1683
|
if (data.success) {
|
|
1647
1684
|
queryMonitorEnabled = true;
|
|
1648
1685
|
currentSessionId = data.session_id;
|
|
1686
|
+
clearQueryFeed();
|
|
1649
1687
|
updateQueryMonitorUI(true);
|
|
1688
|
+
renderQueryMonitorSource();
|
|
1650
1689
|
startQueryMonitorPolling();
|
|
1651
1690
|
showToast(data.message);
|
|
1652
1691
|
} else {
|
|
1653
|
-
showToast(data.message || PG_REPORTS_I18N.errors.start_monitoring_failed, 'error');
|
|
1692
|
+
showToast(data.message || data.error || PG_REPORTS_I18N.errors.start_monitoring_failed, 'error');
|
|
1654
1693
|
button.disabled = false;
|
|
1655
1694
|
button.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
|
|
1656
1695
|
}
|
|
@@ -1677,11 +1716,7 @@ end</pre>
|
|
|
1677
1716
|
const data = await response.json();
|
|
1678
1717
|
|
|
1679
1718
|
if (data.success) {
|
|
1680
|
-
|
|
1681
|
-
currentSessionId = null;
|
|
1682
|
-
stopQueryMonitorPolling();
|
|
1683
|
-
|
|
1684
|
-
updateQueryMonitorUI(false);
|
|
1719
|
+
endQueryMonitoringSession();
|
|
1685
1720
|
showToast(data.message);
|
|
1686
1721
|
} else {
|
|
1687
1722
|
showToast(data.message || PG_REPORTS_I18N.errors.stop_monitoring_failed, 'error');
|
|
@@ -1702,9 +1737,7 @@ end</pre>
|
|
|
1702
1737
|
const stopBtn = document.getElementById('stop-monitor-btn');
|
|
1703
1738
|
const loadHistoryBtn = document.getElementById('load-history-btn');
|
|
1704
1739
|
const sessionBadge = document.getElementById('session-badge');
|
|
1705
|
-
const sessionIdEl = document.getElementById('session-id');
|
|
1706
1740
|
const downloadDropdown = document.getElementById('monitor-download-dropdown');
|
|
1707
|
-
const feed = document.getElementById('query-feed');
|
|
1708
1741
|
|
|
1709
1742
|
panel.style.display = 'block';
|
|
1710
1743
|
|
|
@@ -1716,7 +1749,6 @@ end</pre>
|
|
|
1716
1749
|
stopBtn.innerHTML = PG_REPORTS_I18N.actions.stop_monitoring;
|
|
1717
1750
|
loadHistoryBtn.style.display = 'none'; // Hide when monitoring active
|
|
1718
1751
|
sessionBadge.style.display = 'inline-block';
|
|
1719
|
-
sessionIdEl.textContent = currentSessionId ? currentSessionId.substring(0, 8) : '';
|
|
1720
1752
|
downloadDropdown.style.display = 'inline-block';
|
|
1721
1753
|
} else {
|
|
1722
1754
|
indicator.classList.remove('active');
|
|
@@ -1729,14 +1761,10 @@ end</pre>
|
|
|
1729
1761
|
sessionBadge.style.display = 'none';
|
|
1730
1762
|
|
|
1731
1763
|
// Keep results visible after stopping, only hide download if no queries
|
|
1732
|
-
|
|
1733
|
-
downloadDropdown.style.display = hasQueries ? 'inline-block' : 'none';
|
|
1734
|
-
|
|
1735
|
-
// Only clear feed if no queries captured
|
|
1736
|
-
if (!hasQueries) {
|
|
1737
|
-
feed.innerHTML = '<div class="query-feed-empty">' + PG_REPORTS_I18N.monitoring.feed_empty + '</div>';
|
|
1738
|
-
}
|
|
1764
|
+
downloadDropdown.style.display = queryCount > 0 ? 'inline-block' : 'none';
|
|
1739
1765
|
}
|
|
1766
|
+
|
|
1767
|
+
renderQueryFeedEmptyState(enabled);
|
|
1740
1768
|
}
|
|
1741
1769
|
|
|
1742
1770
|
function toggleMonitorDownloadMenu(event) {
|
|
@@ -1775,126 +1803,206 @@ end</pre>
|
|
|
1775
1803
|
});
|
|
1776
1804
|
|
|
1777
1805
|
function startQueryMonitorPolling() {
|
|
1778
|
-
|
|
1806
|
+
stopQueryMonitorPolling();
|
|
1779
1807
|
|
|
1780
1808
|
fetchQueryFeed(); // Immediate fetch
|
|
1781
1809
|
queryMonitorPollTimer = setInterval(fetchQueryFeed, QUERY_MONITOR_CONFIG.pollInterval);
|
|
1810
|
+
queryMonitorCountdownTimer = setInterval(renderQueryMonitorCountdown, 1000);
|
|
1782
1811
|
}
|
|
1783
1812
|
|
|
1784
1813
|
function stopQueryMonitorPolling() {
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1814
|
+
clearInterval(queryMonitorPollTimer);
|
|
1815
|
+
clearInterval(queryMonitorCountdownTimer);
|
|
1816
|
+
queryMonitorPollTimer = null;
|
|
1817
|
+
queryMonitorCountdownTimer = null;
|
|
1789
1818
|
}
|
|
1790
1819
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1820
|
+
// Shared by the Stop button and a session that ended on its own. Rows stay.
|
|
1821
|
+
function endQueryMonitoringSession() {
|
|
1822
|
+
queryMonitorEnabled = false;
|
|
1823
|
+
stopQueryMonitorPolling();
|
|
1824
|
+
updateQueryMonitorUI(false);
|
|
1825
|
+
renderQueryMonitorSource();
|
|
1826
|
+
setTimeout(fetchQueryFeed, QUERY_MONITOR_CONFIG.settleDelay);
|
|
1827
|
+
}
|
|
1793
1828
|
|
|
1829
|
+
async function fetchQueryFeed() {
|
|
1794
1830
|
try {
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1831
|
+
const params = new URLSearchParams({
|
|
1832
|
+
limit: QUERY_MONITOR_CONFIG.maxQueries,
|
|
1833
|
+
since: queryLastSeq,
|
|
1834
|
+
session_id: currentSessionId || ''
|
|
1835
|
+
});
|
|
1836
|
+
const response = await fetch(`${pgReportsRoot}/query_monitor/feed?${params}`);
|
|
1798
1837
|
const data = await response.json();
|
|
1799
1838
|
|
|
1800
|
-
if (data.success
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
const errorMsg = data.message || data.error || PG_REPORTS_I18N.errors.query_monitoring_error;
|
|
1805
|
-
showToast(errorMsg, 'error');
|
|
1839
|
+
if (!data.success) {
|
|
1840
|
+
renderQueryMonitorError(data.error);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1806
1843
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1844
|
+
renderQueryMonitorError(data.capture_error);
|
|
1845
|
+
const wasEnabled = queryMonitorEnabled;
|
|
1846
|
+
|
|
1847
|
+
// Someone restarted monitoring: the sequence numbers belong to a new session
|
|
1848
|
+
if (data.enabled && data.session_id !== currentSessionId) {
|
|
1849
|
+
currentSessionId = data.session_id;
|
|
1850
|
+
queryMonitorEnabled = true;
|
|
1851
|
+
clearQueryFeed();
|
|
1852
|
+
applyQueryMonitorSources(data);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
applyQueryMonitorStatus(data);
|
|
1856
|
+
appendQueries(data.queries);
|
|
1857
|
+
queryLastSeq = Math.max(queryLastSeq, data.last_seq);
|
|
1858
|
+
|
|
1859
|
+
if (wasEnabled && !data.enabled) {
|
|
1860
|
+
showToast(PG_REPORTS_I18N.monitoring.session_ended);
|
|
1861
|
+
endQueryMonitoringSession();
|
|
1862
|
+
} else if (!queryMonitorEnabled) {
|
|
1863
|
+
// Enough rows arrived after the stop to change what can be downloaded
|
|
1811
1864
|
updateQueryMonitorUI(false);
|
|
1812
1865
|
}
|
|
1813
1866
|
} catch (error) {
|
|
1814
1867
|
console.error('Failed to fetch query feed:', error);
|
|
1815
|
-
showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
|
|
1816
1868
|
}
|
|
1817
1869
|
}
|
|
1818
1870
|
|
|
1871
|
+
function clearQueryFeed() {
|
|
1872
|
+
document.getElementById('query-feed-body').replaceChildren();
|
|
1873
|
+
queryLastSeq = 0;
|
|
1874
|
+
queryCount = 0;
|
|
1875
|
+
document.getElementById('query-count').textContent = '0';
|
|
1876
|
+
renderQueryFeedEmptyState(queryMonitorEnabled);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function renderQueryFeedEmptyState(enabled) {
|
|
1880
|
+
const hasRows = document.getElementById('query-feed-body').children.length > 0;
|
|
1881
|
+
const empty = document.getElementById('query-feed-empty');
|
|
1882
|
+
|
|
1883
|
+
document.getElementById('query-feed-table').style.display = hasRows ? 'table' : 'none';
|
|
1884
|
+
empty.style.display = hasRows ? 'none' : 'block';
|
|
1885
|
+
empty.textContent = enabled ? PG_REPORTS_I18N.monitoring.feed_no_queries : PG_REPORTS_I18N.monitoring.feed_empty;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Replaces the table with a finished list, such as the history from the log file
|
|
1819
1889
|
function renderQueryFeed(queries) {
|
|
1890
|
+
clearQueryFeed();
|
|
1891
|
+
appendQueries(queries);
|
|
1892
|
+
queryCount = queries.length;
|
|
1893
|
+
document.getElementById('query-count').textContent = queryCount;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// Rows are only added, never redrawn, so an expanded query and the reader's
|
|
1897
|
+
// scroll position survive every poll. `queries` arrive oldest first.
|
|
1898
|
+
function appendQueries(queries) {
|
|
1899
|
+
if (queries.length === 0) return;
|
|
1900
|
+
|
|
1820
1901
|
const feed = document.getElementById('query-feed');
|
|
1902
|
+
const body = document.getElementById('query-feed-body');
|
|
1903
|
+
const heightBefore = body.offsetHeight;
|
|
1821
1904
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1905
|
+
queries.forEach(query => body.prepend(buildQueryRow(query)));
|
|
1906
|
+
|
|
1907
|
+
let rows = body.querySelectorAll('.query-row').length;
|
|
1908
|
+
while (rows > QUERY_MONITOR_CONFIG.maxQueries) {
|
|
1909
|
+
if (body.lastElementChild.classList.contains('query-row')) rows--;
|
|
1910
|
+
body.lastElementChild.remove();
|
|
1827
1911
|
}
|
|
1828
1912
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1913
|
+
renderQueryFeedEmptyState(queryMonitorEnabled);
|
|
1914
|
+
|
|
1915
|
+
// Someone reading further down keeps their place as rows arrive on top
|
|
1916
|
+
if (feed.scrollTop > 0) feed.scrollTop += body.offsetHeight - heightBefore;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
function buildQueryCell(className, text) {
|
|
1920
|
+
const cell = document.createElement('td');
|
|
1921
|
+
cell.className = className;
|
|
1922
|
+
cell.textContent = text;
|
|
1923
|
+
return cell;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function buildQuerySourceLink(query) {
|
|
1927
|
+
const location = query.source_location;
|
|
1928
|
+
if (!location || !location.file) return null;
|
|
1831
1929
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1930
|
+
const link = document.createElement('a');
|
|
1931
|
+
link.href = '#';
|
|
1932
|
+
link.className = 'query-source-link';
|
|
1933
|
+
link.dataset.file = location.file;
|
|
1934
|
+
link.dataset.line = location.line;
|
|
1935
|
+
link.title = `${location.file}:${location.line}`;
|
|
1936
|
+
// The tail of the path is what tells two rows apart in a narrow column
|
|
1937
|
+
link.textContent = `${location.file.split('/').slice(-2).join('/')}:${location.line}`;
|
|
1938
|
+
return link;
|
|
1835
1939
|
}
|
|
1836
1940
|
|
|
1837
|
-
function
|
|
1838
|
-
const
|
|
1941
|
+
function buildQueryRow(query) {
|
|
1942
|
+
const row = document.createElement('tr');
|
|
1943
|
+
row.className = 'query-row';
|
|
1944
|
+
queryFeedData.set(row, query);
|
|
1945
|
+
|
|
1946
|
+
const duration = Number(query.duration_ms) || 0;
|
|
1839
1947
|
const durationClass = duration < 10 ? 'fast' : duration > 100 ? 'slow' : '';
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1948
|
+
|
|
1949
|
+
row.append(
|
|
1950
|
+
buildQueryCell('col-time', new Date(query.timestamp).toLocaleTimeString()),
|
|
1951
|
+
buildQueryCell(`col-duration query-duration ${durationClass}`, duration.toFixed(1)),
|
|
1952
|
+
buildQueryCell('col-source', ''),
|
|
1953
|
+
buildQueryCell('col-sql', String(query.sql || '').replace(/\s+/g, ' ').trim())
|
|
1954
|
+
);
|
|
1955
|
+
|
|
1956
|
+
const link = buildQuerySourceLink(query);
|
|
1957
|
+
const sourceCell = row.querySelector('.col-source');
|
|
1958
|
+
if (link) {
|
|
1959
|
+
sourceCell.append(link);
|
|
1960
|
+
} else {
|
|
1961
|
+
sourceCell.textContent = query.source || query.name || PG_REPORTS_I18N.monitoring.unknown_source;
|
|
1850
1962
|
}
|
|
1851
1963
|
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
</div>
|
|
1875
|
-
<div class="query-sql query-sql-expanded" id="${queryId}-sql-full" style="display: none;">
|
|
1876
|
-
${escapeHtml(fullSql)}
|
|
1877
|
-
</div>
|
|
1878
|
-
</div>
|
|
1879
|
-
</div>
|
|
1880
|
-
`;
|
|
1964
|
+
return row;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function buildQueryDetail(query) {
|
|
1968
|
+
const row = document.createElement('tr');
|
|
1969
|
+
row.className = 'query-row-detail';
|
|
1970
|
+
|
|
1971
|
+
const cell = document.createElement('td');
|
|
1972
|
+
cell.colSpan = 4;
|
|
1973
|
+
|
|
1974
|
+
const link = buildQuerySourceLink(query);
|
|
1975
|
+
if (link) {
|
|
1976
|
+
link.textContent = link.title;
|
|
1977
|
+
cell.append(link);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
const sql = document.createElement('pre');
|
|
1981
|
+
sql.textContent = String(query.sql || '').trim();
|
|
1982
|
+
cell.append(sql);
|
|
1983
|
+
|
|
1984
|
+
row.append(cell);
|
|
1985
|
+
return row;
|
|
1881
1986
|
}
|
|
1882
1987
|
|
|
1883
|
-
function
|
|
1884
|
-
const
|
|
1885
|
-
|
|
1886
|
-
|
|
1988
|
+
function handleQueryFeedClick(event) {
|
|
1989
|
+
const link = event.target.closest('.query-source-link');
|
|
1990
|
+
if (link) {
|
|
1991
|
+
openInIDE(link.dataset.file, Number(link.dataset.line), event);
|
|
1992
|
+
event.preventDefault();
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
const row = event.target.closest('.query-row');
|
|
1997
|
+
if (!row || String(window.getSelection())) return;
|
|
1887
1998
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
iconEl.textContent = '▼';
|
|
1999
|
+
const detail = row.nextElementSibling;
|
|
2000
|
+
if (detail && detail.classList.contains('query-row-detail')) {
|
|
2001
|
+
detail.remove();
|
|
2002
|
+
row.classList.remove('expanded');
|
|
1893
2003
|
} else {
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
expandedEl.style.display = 'block';
|
|
1897
|
-
iconEl.textContent = '▲';
|
|
2004
|
+
row.after(buildQueryDetail(queryFeedData.get(row)));
|
|
2005
|
+
row.classList.add('expanded');
|
|
1898
2006
|
}
|
|
1899
2007
|
}
|
|
1900
2008
|
|