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
|
@@ -6,7 +6,7 @@ module PgReports
|
|
|
6
6
|
class MetricsController < ActionController::Base
|
|
7
7
|
CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
|
|
8
8
|
|
|
9
|
-
before_action :authenticate_metrics
|
|
9
|
+
before_action :authenticate_metrics!
|
|
10
10
|
|
|
11
11
|
def show
|
|
12
12
|
render plain: PgReports::Grafana::Exporter.render, content_type: CONTENT_TYPE
|
|
@@ -14,14 +14,23 @@ module PgReports
|
|
|
14
14
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
|
+
# Row labels can hold table names and query text, so without a token this is
|
|
18
|
+
# held to the same rule as the dashboard.
|
|
17
19
|
def authenticate_metrics!
|
|
18
|
-
|
|
20
|
+
token = PgReports.config.grafana_metrics_token
|
|
21
|
+
return if token == AccessPolicy::HOST_MANAGED
|
|
22
|
+
|
|
23
|
+
if token.blank?
|
|
24
|
+
head :forbidden unless AccessPolicy.open_access_allowed?(request)
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
19
28
|
provided = request.headers["Authorization"].to_s.sub(/\Abearer\s+/i, "")
|
|
20
29
|
|
|
21
|
-
a = ::Digest::SHA256.digest(
|
|
30
|
+
a = ::Digest::SHA256.digest(token.to_s)
|
|
22
31
|
b = ::Digest::SHA256.digest(provided)
|
|
23
32
|
|
|
24
|
-
head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(a, b)
|
|
33
|
+
head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(a, b)
|
|
25
34
|
end
|
|
26
35
|
end
|
|
27
36
|
end
|
|
@@ -1089,14 +1089,63 @@
|
|
|
1089
1089
|
font-weight: 600;
|
|
1090
1090
|
}
|
|
1091
1091
|
|
|
1092
|
+
.query-source-switch {
|
|
1093
|
+
display: inline-flex;
|
|
1094
|
+
border: 1px solid var(--border-color);
|
|
1095
|
+
border-radius: var(--radius-sm);
|
|
1096
|
+
overflow: hidden;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
.query-source-switch[hidden] {
|
|
1100
|
+
display: none;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.query-source-switch button {
|
|
1104
|
+
padding: 0.3rem 0.6rem;
|
|
1105
|
+
background: transparent;
|
|
1106
|
+
border: none;
|
|
1107
|
+
color: var(--text-muted);
|
|
1108
|
+
font-size: 0.72rem;
|
|
1109
|
+
cursor: pointer;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.query-source-switch button + button {
|
|
1113
|
+
border-left: 1px solid var(--border-color);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.query-source-switch button.active {
|
|
1117
|
+
background: var(--bg-tertiary);
|
|
1118
|
+
color: var(--text-primary);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.query-source-switch button:disabled {
|
|
1122
|
+
cursor: not-allowed;
|
|
1123
|
+
opacity: 0.6;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.query-scope-warning {
|
|
1127
|
+
margin-bottom: 0.75rem;
|
|
1128
|
+
padding: 0.5rem 0.75rem;
|
|
1129
|
+
font-size: 0.75rem;
|
|
1130
|
+
color: var(--accent-amber);
|
|
1131
|
+
background: var(--amber-soft);
|
|
1132
|
+
border: 1px solid var(--amber-line);
|
|
1133
|
+
border-radius: var(--radius-sm);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.query-monitor-error {
|
|
1137
|
+
color: var(--accent-rose);
|
|
1138
|
+
background: var(--rose-soft);
|
|
1139
|
+
border-color: var(--rose-line);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1092
1142
|
/* Query Feed */
|
|
1093
1143
|
.query-feed {
|
|
1094
|
-
max-height:
|
|
1144
|
+
max-height: 420px;
|
|
1095
1145
|
overflow-y: auto;
|
|
1096
1146
|
background: var(--bg-tertiary);
|
|
1097
1147
|
border: 1px solid var(--border-color);
|
|
1098
1148
|
border-radius: 6px;
|
|
1099
|
-
padding: 0.75rem;
|
|
1100
1149
|
}
|
|
1101
1150
|
|
|
1102
1151
|
.query-feed::-webkit-scrollbar {
|
|
@@ -1124,118 +1173,107 @@
|
|
|
1124
1173
|
font-size: 0.8rem;
|
|
1125
1174
|
}
|
|
1126
1175
|
|
|
1127
|
-
.query-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
font-size: 0.8rem;
|
|
1134
|
-
transition: all 0.15s;
|
|
1176
|
+
.query-feed-table {
|
|
1177
|
+
width: 100%;
|
|
1178
|
+
/* Fixed, so the SQL cell ellipsizes instead of widening the table */
|
|
1179
|
+
table-layout: fixed;
|
|
1180
|
+
border-collapse: collapse;
|
|
1181
|
+
font-size: 0.72rem;
|
|
1135
1182
|
}
|
|
1136
1183
|
|
|
1137
|
-
.query-
|
|
1138
|
-
|
|
1184
|
+
.query-feed-table th {
|
|
1185
|
+
position: sticky;
|
|
1186
|
+
top: 0;
|
|
1187
|
+
z-index: 1;
|
|
1188
|
+
padding: 0.3rem 0.6rem;
|
|
1189
|
+
background: var(--bg-secondary);
|
|
1190
|
+
border-bottom: 1px solid var(--border-color);
|
|
1191
|
+
color: var(--text-muted);
|
|
1192
|
+
font-weight: 500;
|
|
1193
|
+
text-align: left;
|
|
1139
1194
|
}
|
|
1140
1195
|
|
|
1141
|
-
.query-
|
|
1142
|
-
|
|
1196
|
+
.query-feed-table td {
|
|
1197
|
+
padding: 0.2rem 0.6rem;
|
|
1198
|
+
border-bottom: 1px solid var(--border-color);
|
|
1199
|
+
color: var(--text-secondary);
|
|
1200
|
+
white-space: nowrap;
|
|
1201
|
+
overflow: hidden;
|
|
1202
|
+
text-overflow: ellipsis;
|
|
1143
1203
|
}
|
|
1144
1204
|
|
|
1145
|
-
.query-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1205
|
+
.query-feed-table .col-time { width: 6.5rem; }
|
|
1206
|
+
.query-feed-table .col-duration { width: 4.5rem; text-align: right; }
|
|
1207
|
+
.query-feed-table .col-source { width: 15rem; }
|
|
1208
|
+
|
|
1209
|
+
.query-feed-table td.col-time,
|
|
1210
|
+
.query-feed-table td.col-duration {
|
|
1211
|
+
font-variant-numeric: tabular-nums;
|
|
1150
1212
|
}
|
|
1151
1213
|
|
|
1152
|
-
.query-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
font-size: 0.7rem;
|
|
1156
|
-
color: var(--text-muted);
|
|
1214
|
+
.query-feed-table td.col-sql,
|
|
1215
|
+
.query-feed-table td.col-source {
|
|
1216
|
+
font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
|
|
1157
1217
|
}
|
|
1158
1218
|
|
|
1159
|
-
.query-
|
|
1160
|
-
color: var(--text-
|
|
1161
|
-
|
|
1219
|
+
.query-feed-table td.col-sql {
|
|
1220
|
+
color: var(--text-primary);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
.query-row {
|
|
1224
|
+
cursor: pointer;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
.query-row:hover td,
|
|
1228
|
+
.query-row.expanded td {
|
|
1229
|
+
background: var(--bg-card);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
.query-row.expanded td {
|
|
1233
|
+
border-bottom-color: transparent;
|
|
1162
1234
|
}
|
|
1163
1235
|
|
|
1164
|
-
.query-duration {
|
|
1236
|
+
.query-feed-table td.query-duration {
|
|
1165
1237
|
color: var(--accent-amber);
|
|
1166
1238
|
font-weight: 600;
|
|
1167
|
-
font-size: 0.7rem;
|
|
1168
1239
|
}
|
|
1169
1240
|
|
|
1170
|
-
.query-duration.fast {
|
|
1241
|
+
.query-feed-table td.query-duration.fast {
|
|
1171
1242
|
color: var(--accent-green);
|
|
1172
1243
|
}
|
|
1173
1244
|
|
|
1174
|
-
.query-duration.slow {
|
|
1245
|
+
.query-feed-table td.query-duration.slow {
|
|
1175
1246
|
color: var(--accent-rose);
|
|
1176
1247
|
}
|
|
1177
1248
|
|
|
1178
|
-
.query-
|
|
1179
|
-
color: var(--
|
|
1180
|
-
|
|
1181
|
-
font-size: 0.7rem;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
.query-expand-btn {
|
|
1185
|
-
background: transparent;
|
|
1186
|
-
border: none;
|
|
1187
|
-
color: var(--text-muted);
|
|
1188
|
-
cursor: pointer;
|
|
1189
|
-
padding: 0.15rem 0.35rem;
|
|
1190
|
-
font-size: 0.7rem;
|
|
1191
|
-
transition: all 0.15s;
|
|
1192
|
-
border-radius: 3px;
|
|
1249
|
+
.query-source-link {
|
|
1250
|
+
color: var(--accent-purple);
|
|
1251
|
+
text-decoration: none;
|
|
1193
1252
|
}
|
|
1194
1253
|
|
|
1195
|
-
.query-
|
|
1196
|
-
|
|
1197
|
-
color: var(--accent-purple);
|
|
1254
|
+
.query-source-link:hover {
|
|
1255
|
+
text-decoration: underline;
|
|
1198
1256
|
}
|
|
1199
1257
|
|
|
1200
|
-
.query-
|
|
1201
|
-
|
|
1258
|
+
.query-feed-table .query-row-detail td {
|
|
1259
|
+
padding: 0.25rem 0.6rem 0.6rem;
|
|
1260
|
+
background: var(--bg-card);
|
|
1261
|
+
white-space: normal;
|
|
1202
1262
|
}
|
|
1203
1263
|
|
|
1204
|
-
.query-
|
|
1264
|
+
.query-row-detail pre {
|
|
1265
|
+
margin: 0.35rem 0 0;
|
|
1205
1266
|
padding: 0.5rem;
|
|
1267
|
+
max-height: 16rem;
|
|
1268
|
+
overflow: auto;
|
|
1206
1269
|
background: var(--bg-primary);
|
|
1207
1270
|
border: 1px solid var(--border-color);
|
|
1208
1271
|
border-radius: 4px;
|
|
1209
1272
|
font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
|
|
1210
|
-
font-size: 0.
|
|
1211
|
-
color: var(--
|
|
1212
|
-
word-wrap: break-word;
|
|
1213
|
-
overflow-x: auto;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
.query-sql-collapsed {
|
|
1217
|
-
white-space: nowrap;
|
|
1218
|
-
overflow: hidden;
|
|
1219
|
-
text-overflow: ellipsis;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
.query-sql-expanded {
|
|
1273
|
+
font-size: 0.72rem;
|
|
1274
|
+
color: var(--text-primary);
|
|
1223
1275
|
white-space: pre-wrap;
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
.query-source {
|
|
1227
|
-
font-size: 0.7rem;
|
|
1228
|
-
color: var(--text-muted);
|
|
1229
|
-
font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
.query-source a {
|
|
1233
|
-
color: var(--accent-purple);
|
|
1234
|
-
text-decoration: none;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
.query-source a:hover {
|
|
1238
|
-
text-decoration: underline;
|
|
1276
|
+
word-break: break-word;
|
|
1239
1277
|
}
|
|
1240
1278
|
|
|
1241
1279
|
/* IDE Dropdown for query source links */
|
|
@@ -1330,44 +1368,57 @@
|
|
|
1330
1368
|
font-size: 0.9rem;
|
|
1331
1369
|
}
|
|
1332
1370
|
|
|
1333
|
-
.
|
|
1371
|
+
/* Settings modal sections. The option rows are shared by every section —
|
|
1372
|
+
language, IDE, and whatever is added next. */
|
|
1373
|
+
.settings-section-title {
|
|
1374
|
+
font-size: 0.6875rem;
|
|
1375
|
+
font-weight: 600;
|
|
1376
|
+
line-height: 1.4;
|
|
1377
|
+
color: var(--text-muted);
|
|
1378
|
+
text-transform: uppercase;
|
|
1379
|
+
letter-spacing: 0.06em;
|
|
1380
|
+
margin-bottom: 0.5rem;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.settings-options {
|
|
1334
1384
|
display: flex;
|
|
1335
1385
|
flex-direction: column;
|
|
1336
|
-
gap: 0.
|
|
1386
|
+
gap: 0.375rem;
|
|
1337
1387
|
}
|
|
1338
1388
|
|
|
1339
|
-
.
|
|
1389
|
+
.settings-option {
|
|
1340
1390
|
display: flex;
|
|
1341
1391
|
align-items: center;
|
|
1342
|
-
gap: 0.
|
|
1343
|
-
padding: 0.75rem
|
|
1392
|
+
gap: 0.625rem;
|
|
1393
|
+
padding: 0.5rem 0.75rem;
|
|
1344
1394
|
background: var(--bg-tertiary);
|
|
1345
1395
|
border: 1px solid var(--border-color);
|
|
1346
|
-
border-radius:
|
|
1396
|
+
border-radius: var(--radius-sm);
|
|
1347
1397
|
cursor: pointer;
|
|
1348
|
-
transition:
|
|
1398
|
+
transition: background-color 0.12s ease, border-color 0.12s ease;
|
|
1349
1399
|
}
|
|
1350
1400
|
|
|
1351
|
-
.
|
|
1401
|
+
.settings-option:hover {
|
|
1352
1402
|
background: var(--bg-hover);
|
|
1353
1403
|
border-color: var(--border-strong);
|
|
1354
1404
|
}
|
|
1355
1405
|
|
|
1356
|
-
.
|
|
1406
|
+
.settings-option:has(input:checked) {
|
|
1357
1407
|
background: var(--purple-soft);
|
|
1358
1408
|
border-color: var(--purple-line);
|
|
1359
1409
|
}
|
|
1360
1410
|
|
|
1361
|
-
.
|
|
1411
|
+
.settings-option input[type="radio"] {
|
|
1362
1412
|
accent-color: var(--accent-purple);
|
|
1413
|
+
flex: none;
|
|
1363
1414
|
}
|
|
1364
1415
|
|
|
1365
|
-
.
|
|
1416
|
+
.settings-option span {
|
|
1366
1417
|
color: var(--text-secondary);
|
|
1367
|
-
font-size: 0.
|
|
1418
|
+
font-size: 0.8125rem;
|
|
1368
1419
|
}
|
|
1369
1420
|
|
|
1370
|
-
.
|
|
1421
|
+
.settings-option:has(input:checked) span {
|
|
1371
1422
|
color: var(--text-primary);
|
|
1372
1423
|
}
|
|
1373
1424
|
|
|
@@ -1508,6 +1559,14 @@
|
|
|
1508
1559
|
flex-wrap: wrap;
|
|
1509
1560
|
}
|
|
1510
1561
|
|
|
1562
|
+
/* The source is still one tap away, in the expanded row */
|
|
1563
|
+
.query-feed-table .col-source {
|
|
1564
|
+
display: none;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
.query-feed-table .col-time { width: 5rem; }
|
|
1568
|
+
.query-feed-table .col-duration { width: 3.5rem; }
|
|
1569
|
+
|
|
1511
1570
|
/* Anchored to its chip, this tooltip runs off the screen and stretches
|
|
1512
1571
|
the page with it; on a phone it reads as a bottom sheet instead. */
|
|
1513
1572
|
.query-monitoring-scope::after {
|
|
@@ -1634,6 +1693,76 @@
|
|
|
1634
1693
|
].join('');
|
|
1635
1694
|
};
|
|
1636
1695
|
|
|
1696
|
+
// Settings modal. Shared by the dashboard and the report pages; the language
|
|
1697
|
+
// section posts a form (server-side session), the IDE section only writes
|
|
1698
|
+
// localStorage, so there is nothing to submit for it.
|
|
1699
|
+
// Picking a language posts a form and the page reloads (everything on it has
|
|
1700
|
+
// to re-render in the new language), which would leave the user staring at a
|
|
1701
|
+
// dashboard with the settings they were editing gone. Remember that the
|
|
1702
|
+
// modal was open and bring it back on the way in.
|
|
1703
|
+
const SETTINGS_REOPEN_KEY = 'pgReportsSettingsReopen';
|
|
1704
|
+
|
|
1705
|
+
function rememberSettingsOpen() {
|
|
1706
|
+
try {
|
|
1707
|
+
sessionStorage.setItem(SETTINGS_REOPEN_KEY, '1');
|
|
1708
|
+
} catch (e) {
|
|
1709
|
+
// Storage can be unavailable (private mode, blocked cookies); losing the
|
|
1710
|
+
// reopen is a cosmetic downgrade, not a reason to block the switch.
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function consumeSettingsReopen() {
|
|
1715
|
+
try {
|
|
1716
|
+
if (!sessionStorage.getItem(SETTINGS_REOPEN_KEY)) return false;
|
|
1717
|
+
sessionStorage.removeItem(SETTINGS_REOPEN_KEY);
|
|
1718
|
+
return true;
|
|
1719
|
+
} catch (e) {
|
|
1720
|
+
return false;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function showSettingsModal() {
|
|
1725
|
+
const modal = document.getElementById('settings-modal');
|
|
1726
|
+
if (!modal) return;
|
|
1727
|
+
|
|
1728
|
+
modal.style.display = 'flex';
|
|
1729
|
+
loadSettingsState();
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function closeSettingsModal() {
|
|
1733
|
+
const modal = document.getElementById('settings-modal');
|
|
1734
|
+
if (modal) modal.style.display = 'none';
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function setDefaultIde(ideKey) {
|
|
1738
|
+
if (ideKey) {
|
|
1739
|
+
localStorage.setItem('pgReportsDefaultIde', ideKey);
|
|
1740
|
+
} else {
|
|
1741
|
+
localStorage.removeItem('pgReportsDefaultIde');
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function getDefaultIde() {
|
|
1746
|
+
return localStorage.getItem('pgReportsDefaultIde') || '';
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// The language radios are rendered already checked by the server; only the
|
|
1750
|
+
// IDE choice lives in localStorage and has to be restored on open.
|
|
1751
|
+
function loadSettingsState() {
|
|
1752
|
+
const currentIde = getDefaultIde();
|
|
1753
|
+
document.querySelectorAll('input[name="default-ide"]').forEach(radio => {
|
|
1754
|
+
radio.checked = (radio.value === currentIde);
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1759
|
+
document.getElementById('settings-modal')?.addEventListener('click', function(e) {
|
|
1760
|
+
if (e.target === this) closeSettingsModal();
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
if (consumeSettingsReopen()) showSettingsModal();
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1637
1766
|
function showToast(message, type = 'success') {
|
|
1638
1767
|
const toast = document.getElementById('toast');
|
|
1639
1768
|
toast.textContent = message;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<%# Shared by the dashboard and the report pages. Language is persisted server
|
|
2
|
+
side (session, via a form post); the IDE choice is client side in
|
|
3
|
+
localStorage — hence one <form> section and one plain section. %>
|
|
4
|
+
<div id="settings-modal" class="modal" style="display: none;">
|
|
5
|
+
<div class="modal-content modal-small">
|
|
6
|
+
<div class="modal-header">
|
|
7
|
+
<h3><%= t("pg_reports.ui.modals.settings_title") %></h3>
|
|
8
|
+
<button class="modal-close" onclick="closeSettingsModal()" aria-label="<%= t("pg_reports.ui.actions.cancel") %>">×</button>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="modal-body">
|
|
11
|
+
<% if dashboard_locales.size > 1 %>
|
|
12
|
+
<section class="settings-section">
|
|
13
|
+
<h4 class="settings-section-title"><%= t("pg_reports.ui.language.label") %></h4>
|
|
14
|
+
<form class="settings-options"
|
|
15
|
+
action="<%= switch_locale_path %>"
|
|
16
|
+
method="post"
|
|
17
|
+
data-turbo="false">
|
|
18
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
|
19
|
+
<% dashboard_locales.each do |locale| %>
|
|
20
|
+
<%# Each option is named in its own language, not the current one. %>
|
|
21
|
+
<label class="settings-option">
|
|
22
|
+
<input type="radio"
|
|
23
|
+
name="locale"
|
|
24
|
+
value="<%= locale %>"
|
|
25
|
+
<%= "checked" if locale.to_s == selected_locale.to_s %>
|
|
26
|
+
onchange="rememberSettingsOpen(); this.form.submit()">
|
|
27
|
+
<span><%= t("pg_reports.ui.language.name", locale: locale, default: locale.to_s.upcase) %></span>
|
|
28
|
+
</label>
|
|
29
|
+
<% end %>
|
|
30
|
+
</form>
|
|
31
|
+
</section>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<section class="settings-section">
|
|
35
|
+
<h4 class="settings-section-title"><%= t("pg_reports.ui.settings.default_ide_label") %></h4>
|
|
36
|
+
<div class="settings-options">
|
|
37
|
+
<% {
|
|
38
|
+
"" => "ide_show_menu",
|
|
39
|
+
"vscode-wsl" => "ide_vscode_wsl",
|
|
40
|
+
"vscode" => "ide_vscode",
|
|
41
|
+
"rubymine" => "ide_rubymine",
|
|
42
|
+
"intellij" => "ide_intellij",
|
|
43
|
+
"cursor-wsl" => "ide_cursor_wsl",
|
|
44
|
+
"cursor" => "ide_cursor"
|
|
45
|
+
}.each do |value, key| %>
|
|
46
|
+
<label class="settings-option">
|
|
47
|
+
<input type="radio" name="default-ide" value="<%= value %>" onchange="setDefaultIde('<%= value %>')">
|
|
48
|
+
<span><%= t("pg_reports.ui.settings.#{key}") %></span>
|
|
49
|
+
</label>
|
|
50
|
+
<% end %>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
@@ -1,45 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
<div id="ide-settings-modal" class="modal" style="display: none;">
|
|
3
|
-
<div class="modal-content modal-small">
|
|
4
|
-
<div class="modal-header">
|
|
5
|
-
<h3><%= t("pg_reports.ui.modals.ide_settings_title") %></h3>
|
|
6
|
-
<button class="modal-close" onclick="closeIdeSettingsModal()">×</button>
|
|
7
|
-
</div>
|
|
8
|
-
<div class="modal-body">
|
|
9
|
-
<p class="settings-label"><%= t("pg_reports.ui.settings.default_ide_label") %></p>
|
|
10
|
-
<div class="ide-options">
|
|
11
|
-
<label class="ide-option">
|
|
12
|
-
<input type="radio" name="default-ide" value="" onchange="setDefaultIde('')">
|
|
13
|
-
<span><%= t("pg_reports.ui.settings.ide_show_menu") %></span>
|
|
14
|
-
</label>
|
|
15
|
-
<label class="ide-option">
|
|
16
|
-
<input type="radio" name="default-ide" value="vscode-wsl" onchange="setDefaultIde('vscode-wsl')">
|
|
17
|
-
<span><%= t("pg_reports.ui.settings.ide_vscode_wsl") %></span>
|
|
18
|
-
</label>
|
|
19
|
-
<label class="ide-option">
|
|
20
|
-
<input type="radio" name="default-ide" value="vscode" onchange="setDefaultIde('vscode')">
|
|
21
|
-
<span><%= t("pg_reports.ui.settings.ide_vscode") %></span>
|
|
22
|
-
</label>
|
|
23
|
-
<label class="ide-option">
|
|
24
|
-
<input type="radio" name="default-ide" value="rubymine" onchange="setDefaultIde('rubymine')">
|
|
25
|
-
<span><%= t("pg_reports.ui.settings.ide_rubymine") %></span>
|
|
26
|
-
</label>
|
|
27
|
-
<label class="ide-option">
|
|
28
|
-
<input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
|
|
29
|
-
<span><%= t("pg_reports.ui.settings.ide_intellij") %></span>
|
|
30
|
-
</label>
|
|
31
|
-
<label class="ide-option">
|
|
32
|
-
<input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
|
|
33
|
-
<span><%= t("pg_reports.ui.settings.ide_cursor_wsl") %></span>
|
|
34
|
-
</label>
|
|
35
|
-
<label class="ide-option">
|
|
36
|
-
<input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
|
|
37
|
-
<span><%= t("pg_reports.ui.settings.ide_cursor") %></span>
|
|
38
|
-
</label>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
1
|
+
<%= render "settings_modal" %>
|
|
43
2
|
|
|
44
3
|
<!-- Problem Explanation Modal -->
|
|
45
4
|
<div id="problem-modal" class="problem-modal" style="display: none;">
|