pg_reports 0.5.1 → 0.5.3
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 +56 -5
- data/README.md +3 -1
- data/app/views/layouts/pg_reports/application.html.erb +103 -0
- data/app/views/pg_reports/dashboard/index.html.erb +198 -1
- data/lib/pg_reports/modules/system.rb +20 -9
- data/lib/pg_reports/query_monitor.rb +3 -3
- data/lib/pg_reports/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e9a2129fba68b259fc72598215a7cc02ff8785c93a786603505d9b9987d5df1
|
|
4
|
+
data.tar.gz: 7910113c5cc18115f59463067ab201f1942ec8b968570557af8aefda645098c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e5573dd2cca17cc74cdfe104e6eb09249069d59d50b033effd15e9c46bcb63040da50d998d91c8a2c7e31b8dc4fd3f86c2692f5b60d83050db766942d9a5d46
|
|
7
|
+
data.tar.gz: 025a5a37c86817e22265aff1227e332a1f5dd09cf0954a48321fe1ca5dc8c8f402cd13671c4b7b4c1b69e7ec739ef7f8ca47280b11894718d1bcaa946d7983b7
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.3] - 2026-02-11
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Live Query Monitor** - fixed filtering that was too aggressive:
|
|
15
|
+
- Removed `dashboard_controller.rb` from query filtering to allow monitoring user application queries
|
|
16
|
+
- Now correctly shows queries from user's application even when dashboard page is active
|
|
17
|
+
- Only internal pg_reports module queries are filtered (as intended)
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **IDE Integration for Live Query Monitor**:
|
|
22
|
+
- Clickable source file links in query monitor now open files in IDE
|
|
23
|
+
- Support for multiple IDEs: VS Code, RubyMine, IntelliJ IDEA, Cursor (with WSL variants)
|
|
24
|
+
- Smart IDE selection: shows menu or opens directly if favorite IDE is set
|
|
25
|
+
- IDE settings button (⚙️) in dashboard header for choosing default IDE
|
|
26
|
+
- Settings persist in localStorage across all dashboard pages
|
|
27
|
+
|
|
28
|
+
## [0.5.2] - 2026-02-09
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **pg_stat_statements detection no longer requires `pg_read_all_settings` role**:
|
|
33
|
+
- Changed `pg_stat_statements_preloaded?` to query pg_stat_statements directly instead of checking `shared_preload_libraries`
|
|
34
|
+
- Fixes permission denied errors in environments like CloudnativePG where regular users lack access to `shared_preload_libraries` setting
|
|
35
|
+
- Works seamlessly with Kubernetes PostgreSQL operators and managed databases with restricted permissions
|
|
36
|
+
- Improved error messages in `enable_pg_stat_statements!` method
|
|
37
|
+
|
|
10
38
|
## [0.5.1] - 2026-02-09
|
|
11
39
|
|
|
12
40
|
### Added
|
|
@@ -15,23 +43,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
43
|
- New config option `allow_raw_query_execution` (default: `false`)
|
|
16
44
|
- Environment variable support: `PG_REPORTS_ALLOW_RAW_QUERY_EXECUTION`
|
|
17
45
|
- Security documentation in README with examples and best practices
|
|
18
|
-
- Frontend UI: disabled buttons with tooltips when feature is off
|
|
19
|
-
- JavaScript validation in `executeQuery()` and `executeExplainAnalyze()` functions
|
|
20
46
|
- Configuration tests for new security setting
|
|
47
|
+
- **Hash-based Query Execution System** - prevents SQL injection and query tampering:
|
|
48
|
+
- Backend generates SHA256 hash for each query and stores in Rails.cache (1-hour TTL)
|
|
49
|
+
- Frontend sends only hash (not query text) to execution endpoints
|
|
50
|
+
- Backend retrieves and validates original query by hash
|
|
51
|
+
- Strict validation: only SELECT queries, no dangerous keywords, no multiple statements
|
|
52
|
+
- Cache failure handling with clear error messages
|
|
53
|
+
- Protection against nested SQL injection attempts
|
|
54
|
+
- **Enhanced Error Handling** - improved user feedback:
|
|
55
|
+
- Active warning messages instead of disabled buttons when feature is off
|
|
56
|
+
- Toast notifications with configuration instructions
|
|
57
|
+
- Detailed error messages in modal with code examples
|
|
58
|
+
- Clear messaging when Redis/cache backend is unavailable
|
|
21
59
|
|
|
22
60
|
### Changed
|
|
23
61
|
|
|
24
62
|
- **BREAKING CHANGE**: `execute_query` and `explain_analyze` endpoints now require explicit opt-in
|
|
25
63
|
- Both endpoints return 403 Forbidden when `allow_raw_query_execution` is `false` (default)
|
|
26
|
-
- Dashboard "Execute Query" and "EXPLAIN ANALYZE" buttons disabled by default
|
|
27
64
|
- To restore previous behavior, add to initializer: `config.allow_raw_query_execution = true`
|
|
28
65
|
- **Migration path**: Users must explicitly enable this feature if they were using Query Analyzer
|
|
66
|
+
- **UI/UX Improvements**:
|
|
67
|
+
- Query Analyzer modal size increased: width 600px→900px, height 80vh→90vh for better query visibility
|
|
68
|
+
- "EXPLAIN ANALYZE", "Execute Query", and "Create Migration" buttons now show active warnings when clicked (instead of being disabled)
|
|
69
|
+
- Warning messages include configuration instructions with code examples
|
|
70
|
+
- Better visual feedback for disabled features
|
|
71
|
+
- **Query Execution Flow**:
|
|
72
|
+
- `execute_query` and `explain_analyze` endpoints now accept `query_hash` parameter (instead of `query`)
|
|
73
|
+
- New helper methods: `store_query_with_hash()` and `retrieve_query_by_hash()`
|
|
74
|
+
- Frontend stores `data-query-hash` attribute on EXPLAIN ANALYZE buttons
|
|
75
|
+
- JavaScript validation happens client-side before API calls
|
|
29
76
|
|
|
30
77
|
### Security
|
|
31
78
|
|
|
32
|
-
- Raw SQL execution from dashboard is now **disabled by default** to prevent unauthorized data access
|
|
79
|
+
- **Critical Security Enhancement**: Raw SQL execution from dashboard is now **disabled by default** to prevent unauthorized data access
|
|
80
|
+
- **Query Tampering Prevention**: Frontend cannot modify queries - hash-based verification ensures query integrity
|
|
81
|
+
- **SQL Injection Protection**: Strict validation on backend prevents any non-SELECT queries or dangerous keywords
|
|
82
|
+
- **Multiple Statement Prevention**: Semicolon detection blocks SQL injection attempts with multiple statements
|
|
83
|
+
- **Cache Dependency**: Query execution temporarily disabled if Redis/cache backend is unavailable (fail-secure)
|
|
33
84
|
- Recommended setup: only enable in development/staging environments
|
|
34
|
-
- Existing safety measures (
|
|
85
|
+
- Existing safety measures (automatic LIMIT) still apply when enabled
|
|
35
86
|
|
|
36
87
|
## [0.5.0] - 2026-02-07
|
|
37
88
|
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PgReports
|
|
2
2
|
|
|
3
|
-
[](https://rubygems.org/gems/pg_reports)
|
|
4
4
|
[](https://www.ruby-lang.org/)
|
|
5
5
|
[](https://rubyonrails.org/)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -277,6 +277,8 @@ For query analysis, you need to enable `pg_stat_statements`:
|
|
|
277
277
|
PgReports.enable_pg_stat_statements!
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
+
> **Note**: PgReports does **not** require the `pg_read_all_settings` role. It detects `pg_stat_statements` availability by directly querying the extension, making it compatible with CloudnativePG, managed databases, and other environments with restricted permissions.
|
|
281
|
+
|
|
280
282
|
## Report Object
|
|
281
283
|
|
|
282
284
|
Every method returns a `PgReports::Report` object:
|
|
@@ -776,6 +776,53 @@
|
|
|
776
776
|
text-decoration: underline;
|
|
777
777
|
}
|
|
778
778
|
|
|
779
|
+
/* IDE Dropdown for query source links */
|
|
780
|
+
.ide-dropdown-overlay {
|
|
781
|
+
position: fixed;
|
|
782
|
+
inset: 0;
|
|
783
|
+
z-index: 999;
|
|
784
|
+
display: none;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.ide-dropdown-overlay.show {
|
|
788
|
+
display: block;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.ide-menu {
|
|
792
|
+
position: fixed;
|
|
793
|
+
background: var(--bg-card);
|
|
794
|
+
border: 1px solid var(--border-color);
|
|
795
|
+
border-radius: 6px;
|
|
796
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
797
|
+
z-index: 1000;
|
|
798
|
+
min-width: 160px;
|
|
799
|
+
overflow: hidden;
|
|
800
|
+
display: none;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.ide-menu.show {
|
|
804
|
+
display: block;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.ide-menu a {
|
|
808
|
+
display: block;
|
|
809
|
+
padding: 0.6rem 0.875rem;
|
|
810
|
+
color: var(--text-secondary);
|
|
811
|
+
text-decoration: none;
|
|
812
|
+
font-size: 0.75rem;
|
|
813
|
+
transition: all 0.15s ease;
|
|
814
|
+
border-bottom: 1px solid var(--border-color);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.ide-menu a:last-child {
|
|
818
|
+
border-bottom: none;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.ide-menu a:hover {
|
|
822
|
+
background: var(--bg-tertiary);
|
|
823
|
+
color: var(--text-primary);
|
|
824
|
+
}
|
|
825
|
+
|
|
779
826
|
/* Download Dropdown */
|
|
780
827
|
.download-dropdown {
|
|
781
828
|
position: relative;
|
|
@@ -831,6 +878,58 @@
|
|
|
831
878
|
opacity: 0.9;
|
|
832
879
|
transform: translateY(-1px);
|
|
833
880
|
}
|
|
881
|
+
|
|
882
|
+
/* IDE Settings Modal */
|
|
883
|
+
.settings-label {
|
|
884
|
+
color: var(--text-secondary);
|
|
885
|
+
margin-bottom: 1rem;
|
|
886
|
+
font-size: 0.9rem;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.ide-options {
|
|
890
|
+
display: flex;
|
|
891
|
+
flex-direction: column;
|
|
892
|
+
gap: 0.5rem;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.ide-option {
|
|
896
|
+
display: flex;
|
|
897
|
+
align-items: center;
|
|
898
|
+
gap: 0.75rem;
|
|
899
|
+
padding: 0.75rem 1rem;
|
|
900
|
+
background: var(--bg-tertiary);
|
|
901
|
+
border: 1px solid var(--border-color);
|
|
902
|
+
border-radius: 8px;
|
|
903
|
+
cursor: pointer;
|
|
904
|
+
transition: all 0.15s;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.ide-option:hover {
|
|
908
|
+
background: var(--bg-secondary);
|
|
909
|
+
border-color: var(--accent-purple);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.ide-option:has(input:checked) {
|
|
913
|
+
background: rgba(157, 140, 214, 0.15);
|
|
914
|
+
border-color: var(--accent-purple);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.ide-option input[type="radio"] {
|
|
918
|
+
accent-color: var(--accent-purple);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.ide-option span {
|
|
922
|
+
color: var(--text-secondary);
|
|
923
|
+
font-size: 0.875rem;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.ide-option:has(input:checked) span {
|
|
927
|
+
color: var(--text-primary);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.modal-small {
|
|
931
|
+
max-width: 360px;
|
|
932
|
+
}
|
|
834
933
|
</style>
|
|
835
934
|
</head>
|
|
836
935
|
<body>
|
|
@@ -840,6 +939,10 @@
|
|
|
840
939
|
|
|
841
940
|
<div id="toast" class="toast"></div>
|
|
842
941
|
|
|
942
|
+
<!-- IDE Dropdown overlay and menu -->
|
|
943
|
+
<div id="ide-dropdown-overlay" class="ide-dropdown-overlay" onclick="closeIdeMenu()"></div>
|
|
944
|
+
<div id="ide-menu" class="ide-menu"></div>
|
|
945
|
+
|
|
843
946
|
<script>
|
|
844
947
|
const pgReportsRoot = document.querySelector('meta[name="pg-reports-root"]')?.content || '/pg_reports';
|
|
845
948
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
</button>
|
|
21
21
|
<button class="btn-info" onclick="showPgStatInfo()">?</button>
|
|
22
22
|
<% end %>
|
|
23
|
+
<button class="btn-info" onclick="showIdeSettingsModal()" title="IDE Settings">⚙️</button>
|
|
23
24
|
<div class="header-badge" id="pg-stat-badge">
|
|
24
25
|
<% if @pg_stat_status[:ready] %>
|
|
25
26
|
<span class="badge-dot"></span>
|
|
@@ -85,6 +86,49 @@ pg_stat_statements.track = all</pre>
|
|
|
85
86
|
</div>
|
|
86
87
|
</div>
|
|
87
88
|
|
|
89
|
+
<!-- IDE Settings Modal -->
|
|
90
|
+
<div id="ide-settings-modal" class="modal" style="display: none;">
|
|
91
|
+
<div class="modal-content modal-small">
|
|
92
|
+
<div class="modal-header">
|
|
93
|
+
<h3>⚙️ IDE Settings</h3>
|
|
94
|
+
<button class="modal-close" onclick="closeIdeSettingsModal()">×</button>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="modal-body">
|
|
97
|
+
<p class="settings-label">Default IDE for source links:</p>
|
|
98
|
+
<div class="ide-options">
|
|
99
|
+
<label class="ide-option">
|
|
100
|
+
<input type="radio" name="default-ide" value="" onchange="setDefaultIde('')">
|
|
101
|
+
<span>Show menu (default)</span>
|
|
102
|
+
</label>
|
|
103
|
+
<label class="ide-option">
|
|
104
|
+
<input type="radio" name="default-ide" value="vscode-wsl" onchange="setDefaultIde('vscode-wsl')">
|
|
105
|
+
<span>VS Code (WSL)</span>
|
|
106
|
+
</label>
|
|
107
|
+
<label class="ide-option">
|
|
108
|
+
<input type="radio" name="default-ide" value="vscode" onchange="setDefaultIde('vscode')">
|
|
109
|
+
<span>VS Code</span>
|
|
110
|
+
</label>
|
|
111
|
+
<label class="ide-option">
|
|
112
|
+
<input type="radio" name="default-ide" value="rubymine" onchange="setDefaultIde('rubymine')">
|
|
113
|
+
<span>RubyMine</span>
|
|
114
|
+
</label>
|
|
115
|
+
<label class="ide-option">
|
|
116
|
+
<input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
|
|
117
|
+
<span>IntelliJ IDEA</span>
|
|
118
|
+
</label>
|
|
119
|
+
<label class="ide-option">
|
|
120
|
+
<input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
|
|
121
|
+
<span>Cursor (WSL)</span>
|
|
122
|
+
</label>
|
|
123
|
+
<label class="ide-option">
|
|
124
|
+
<input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
|
|
125
|
+
<span>Cursor</span>
|
|
126
|
+
</label>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
88
132
|
<!-- Live Monitoring Panel -->
|
|
89
133
|
<div id="live-monitoring" class="live-monitoring-panel" style="display: none;">
|
|
90
134
|
<div class="live-monitoring-header">
|
|
@@ -1131,6 +1175,159 @@ pg_stat_statements.track = all</pre>
|
|
|
1131
1175
|
document.addEventListener('DOMContentLoaded', initLiveMonitoring);
|
|
1132
1176
|
window.addEventListener('beforeunload', stopPolling);
|
|
1133
1177
|
|
|
1178
|
+
// ============================================
|
|
1179
|
+
// IDE Integration
|
|
1180
|
+
// ============================================
|
|
1181
|
+
|
|
1182
|
+
const railsRootPath = '<%= Rails.root.to_s %>';
|
|
1183
|
+
const wslDistro = 'Ubuntu';
|
|
1184
|
+
|
|
1185
|
+
const ideKeyMap = {
|
|
1186
|
+
'vscode-wsl': 0,
|
|
1187
|
+
'vscode': 1,
|
|
1188
|
+
'rubymine': 2,
|
|
1189
|
+
'intellij': 3,
|
|
1190
|
+
'cursor-wsl': 4,
|
|
1191
|
+
'cursor': 5
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
function getDefaultIde() {
|
|
1195
|
+
return localStorage.getItem('pgReportsDefaultIde') || '';
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function generateIdeUrls(filePath, lineNumber) {
|
|
1199
|
+
if (!filePath) return [];
|
|
1200
|
+
|
|
1201
|
+
// Convert relative paths to absolute paths using Rails.root
|
|
1202
|
+
let absolutePath = filePath;
|
|
1203
|
+
if (!filePath.startsWith('/')) {
|
|
1204
|
+
absolutePath = `${railsRootPath}/${filePath}`;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const line = lineNumber || 1;
|
|
1208
|
+
const urls = [];
|
|
1209
|
+
|
|
1210
|
+
// VSCode (WSL Remote format)
|
|
1211
|
+
urls.push({
|
|
1212
|
+
name: 'VS Code (WSL)',
|
|
1213
|
+
url: `vscode://vscode-remote/wsl+${wslDistro}${absolutePath}:${line}`
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
// VSCode (direct path)
|
|
1217
|
+
urls.push({
|
|
1218
|
+
name: 'VS Code',
|
|
1219
|
+
url: `vscode://file${absolutePath}:${line}`
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// RubyMine
|
|
1223
|
+
urls.push({
|
|
1224
|
+
name: 'RubyMine',
|
|
1225
|
+
url: `rubymine://open?file=${absolutePath}&line=${line}`
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// IntelliJ
|
|
1229
|
+
urls.push({
|
|
1230
|
+
name: 'IntelliJ',
|
|
1231
|
+
url: `idea://open?file=${absolutePath}&line=${line}`
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
// Cursor (WSL Remote format)
|
|
1235
|
+
urls.push({
|
|
1236
|
+
name: 'Cursor (WSL)',
|
|
1237
|
+
url: `cursor://vscode-remote/wsl+${wslDistro}${absolutePath}:${line}`
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
// Cursor (direct path)
|
|
1241
|
+
urls.push({
|
|
1242
|
+
name: 'Cursor',
|
|
1243
|
+
url: `cursor://file${absolutePath}:${line}`
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
return urls;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function openInIDE(filePath, lineNumber, event) {
|
|
1250
|
+
if (event) {
|
|
1251
|
+
event.preventDefault();
|
|
1252
|
+
event.stopPropagation();
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const ideUrls = generateIdeUrls(filePath, lineNumber);
|
|
1256
|
+
if (ideUrls.length === 0) return;
|
|
1257
|
+
|
|
1258
|
+
const defaultIde = getDefaultIde();
|
|
1259
|
+
|
|
1260
|
+
// If default IDE is set, open directly
|
|
1261
|
+
if (defaultIde && ideKeyMap[defaultIde] !== undefined) {
|
|
1262
|
+
const ideUrl = ideUrls[ideKeyMap[defaultIde]];
|
|
1263
|
+
if (ideUrl) {
|
|
1264
|
+
window.location.href = ideUrl.url;
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// No default IDE - show dropdown menu
|
|
1270
|
+
const menu = document.getElementById('ide-menu');
|
|
1271
|
+
const overlay = document.getElementById('ide-dropdown-overlay');
|
|
1272
|
+
|
|
1273
|
+
let menuHtml = '';
|
|
1274
|
+
ideUrls.forEach(ide => {
|
|
1275
|
+
menuHtml += `<a href="${ide.url}" onclick="closeIdeMenu()">${ide.name}</a>`;
|
|
1276
|
+
});
|
|
1277
|
+
menu.innerHTML = menuHtml;
|
|
1278
|
+
|
|
1279
|
+
// Position menu near the clicked element
|
|
1280
|
+
if (event && event.target) {
|
|
1281
|
+
const rect = event.target.getBoundingClientRect();
|
|
1282
|
+
menu.style.top = (rect.bottom + 4) + 'px';
|
|
1283
|
+
menu.style.left = rect.left + 'px';
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Show menu and overlay
|
|
1287
|
+
menu.classList.add('show');
|
|
1288
|
+
overlay.classList.add('show');
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function closeIdeMenu() {
|
|
1292
|
+
document.getElementById('ide-menu')?.classList.remove('show');
|
|
1293
|
+
document.getElementById('ide-dropdown-overlay')?.classList.remove('show');
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function showIdeSettingsModal() {
|
|
1297
|
+
const modal = document.getElementById('ide-settings-modal');
|
|
1298
|
+
modal.style.display = 'flex';
|
|
1299
|
+
loadIdeSettingsState();
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function closeIdeSettingsModal() {
|
|
1303
|
+
document.getElementById('ide-settings-modal').style.display = 'none';
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function setDefaultIde(ideKey) {
|
|
1307
|
+
if (ideKey) {
|
|
1308
|
+
localStorage.setItem('pgReportsDefaultIde', ideKey);
|
|
1309
|
+
} else {
|
|
1310
|
+
localStorage.removeItem('pgReportsDefaultIde');
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function loadIdeSettingsState() {
|
|
1315
|
+
const currentIde = getDefaultIde();
|
|
1316
|
+
const radios = document.querySelectorAll('input[name="default-ide"]');
|
|
1317
|
+
radios.forEach(radio => {
|
|
1318
|
+
radio.checked = (radio.value === currentIde);
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Close IDE settings modal on backdrop click
|
|
1323
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1324
|
+
document.getElementById('ide-settings-modal')?.addEventListener('click', function(e) {
|
|
1325
|
+
if (e.target === this) {
|
|
1326
|
+
closeIdeSettingsModal();
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1134
1331
|
// ============================================
|
|
1135
1332
|
// Query Monitoring
|
|
1136
1333
|
// ============================================
|
|
@@ -1405,7 +1602,7 @@ pg_stat_statements.track = all</pre>
|
|
|
1405
1602
|
if (query.source_location && query.source_location.file) {
|
|
1406
1603
|
const file = query.source_location.file;
|
|
1407
1604
|
const line = query.source_location.line;
|
|
1408
|
-
sourceInfo = `<a href="#" onclick="openInIDE('${escapeHtml(file)}', ${line}); return false;">
|
|
1605
|
+
sourceInfo = `<a href="#" onclick="openInIDE('${escapeHtml(file)}', ${line}, event); return false;">
|
|
1409
1606
|
${escapeHtml(file)}:${line}
|
|
1410
1607
|
</a>`;
|
|
1411
1608
|
}
|
|
@@ -25,14 +25,19 @@ module PgReports
|
|
|
25
25
|
result.first&.fetch("available", false) || false
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Check if pg_stat_statements is
|
|
28
|
+
# Check if pg_stat_statements is preloaded and functional
|
|
29
29
|
# @return [Boolean] Whether pg_stat_statements is preloaded
|
|
30
|
+
# @note This method tries to query pg_stat_statements directly instead of
|
|
31
|
+
# checking shared_preload_libraries, which requires pg_read_all_settings role
|
|
30
32
|
def pg_stat_statements_preloaded?
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
# If extension is not installed, it can't be preloaded
|
|
34
|
+
return false unless pg_stat_statements_available?
|
|
35
|
+
|
|
36
|
+
# Try to query pg_stat_statements - if it works, it's properly preloaded
|
|
37
|
+
executor.execute("SELECT 1 FROM pg_stat_statements LIMIT 1")
|
|
38
|
+
true
|
|
39
|
+
rescue
|
|
40
|
+
false
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
# Get pg_stat_statements status details
|
|
@@ -94,14 +99,20 @@ module PgReports
|
|
|
94
99
|
executor.execute("CREATE EXTENSION IF NOT EXISTS pg_stat_statements")
|
|
95
100
|
|
|
96
101
|
# Verify it worked
|
|
97
|
-
if pg_stat_statements_available?
|
|
102
|
+
if pg_stat_statements_available? && pg_stat_statements_preloaded?
|
|
98
103
|
{success: true, message: "pg_stat_statements extension created successfully"}
|
|
99
|
-
|
|
104
|
+
elsif pg_stat_statements_available?
|
|
100
105
|
{
|
|
101
106
|
success: false,
|
|
102
|
-
message: "Extension created but not
|
|
107
|
+
message: "Extension created but not preloaded. Add 'pg_stat_statements' to shared_preload_libraries in postgresql.conf and restart PostgreSQL.",
|
|
103
108
|
requires_restart: true
|
|
104
109
|
}
|
|
110
|
+
else
|
|
111
|
+
{
|
|
112
|
+
success: false,
|
|
113
|
+
message: "Failed to create extension. Check database permissions.",
|
|
114
|
+
requires_restart: false
|
|
115
|
+
}
|
|
105
116
|
end
|
|
106
117
|
rescue => e
|
|
107
118
|
error_message = e.message
|
|
@@ -200,10 +200,10 @@ module PgReports
|
|
|
200
200
|
# Filter queries from pg_reports internal modules only:
|
|
201
201
|
# - Installed gem: /gems/pg_reports-X.Y.Z/lib/
|
|
202
202
|
# - Local gem: /pg_reports/lib/pg_reports/modules/
|
|
203
|
-
#
|
|
203
|
+
# Note: We intentionally DO NOT filter dashboard_controller.rb
|
|
204
|
+
# to allow monitoring of user application queries made during dashboard page loads
|
|
204
205
|
path.match?(%r{/gems/pg_reports[-\d.]+/lib/}) ||
|
|
205
|
-
path.match?(%r{/pg_reports/lib/pg_reports/modules/})
|
|
206
|
-
path.match?(%r{/pg_reports/app/controllers/pg_reports/dashboard_controller\.rb})
|
|
206
|
+
path.match?(%r{/pg_reports/lib/pg_reports/modules/})
|
|
207
207
|
end
|
|
208
208
|
end
|
|
209
209
|
|
data/lib/pg_reports/version.rb
CHANGED