pg_reports 0.6.0 → 0.6.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 +45 -0
- data/README.md +143 -378
- data/app/controllers/pg_reports/dashboard_controller.rb +21 -21
- data/app/views/layouts/pg_reports/application.html.erb +65 -8
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +22 -22
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +55 -57
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +18 -0
- data/app/views/pg_reports/dashboard/index.html.erb +109 -106
- data/app/views/pg_reports/dashboard/show.html.erb +26 -26
- data/config/locales/en.yml +488 -0
- data/config/locales/ru.yml +481 -0
- data/config/locales/uk.yml +481 -0
- data/lib/pg_reports/annotation_parser.rb +13 -1
- data/lib/pg_reports/compatibility.rb +3 -3
- data/lib/pg_reports/dashboard/reports_registry.rb +83 -12
- data/lib/pg_reports/definitions/schema_analysis/always_null_columns.yml +31 -0
- data/lib/pg_reports/definitions/schema_analysis/unused_columns.yml +32 -0
- data/lib/pg_reports/definitions/tables/unused_tables.yml +30 -0
- data/lib/pg_reports/definitions/tables/update_hotspots.yml +32 -0
- data/lib/pg_reports/module_generator.rb +2 -1
- data/lib/pg_reports/modules/schema_analysis.rb +261 -2
- data/lib/pg_reports/modules/system.rb +3 -3
- data/lib/pg_reports/query_monitor.rb +2 -6
- data/lib/pg_reports/report_definition.rb +20 -24
- data/lib/pg_reports/sql/schema_analysis/always_null_columns.sql +25 -0
- data/lib/pg_reports/sql/schema_analysis/unused_columns.sql +36 -0
- data/lib/pg_reports/sql/tables/unused_tables.sql +19 -0
- data/lib/pg_reports/sql/tables/update_hotspots.sql +26 -0
- data/lib/pg_reports/version.rb +1 -1
- metadata +9 -1
|
@@ -19,7 +19,7 @@ module PgReports
|
|
|
19
19
|
|
|
20
20
|
def reset_statistics
|
|
21
21
|
PgReports.reset_statistics!
|
|
22
|
-
render json: {success: true, message: "
|
|
22
|
+
render json: {success: true, message: I18n.t("pg_reports.ui.success.statistics_reset")}
|
|
23
23
|
rescue => e
|
|
24
24
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
25
25
|
end
|
|
@@ -35,7 +35,7 @@ module PgReports
|
|
|
35
35
|
if data[:connections][:total].nil? && data[:transactions][:total].nil?
|
|
36
36
|
render json: {
|
|
37
37
|
success: false,
|
|
38
|
-
error: "
|
|
38
|
+
error: I18n.t("pg_reports.ui.errors.fetch_metrics_check_perms"),
|
|
39
39
|
available: false
|
|
40
40
|
}, status: :service_unavailable
|
|
41
41
|
return
|
|
@@ -50,7 +50,7 @@ module PgReports
|
|
|
50
50
|
rescue PG::InsufficientPrivilege
|
|
51
51
|
render json: {
|
|
52
52
|
success: false,
|
|
53
|
-
error: "
|
|
53
|
+
error: I18n.t("pg_reports.ui.errors.insufficient_database_perms"),
|
|
54
54
|
available: false
|
|
55
55
|
}, status: :forbidden
|
|
56
56
|
rescue => e
|
|
@@ -68,7 +68,7 @@ module PgReports
|
|
|
68
68
|
@report_info = Dashboard::ReportsRegistry.find(@category, @report_key)
|
|
69
69
|
|
|
70
70
|
if @report_info.nil?
|
|
71
|
-
redirect_to root_path, alert: "
|
|
71
|
+
redirect_to root_path, alert: I18n.t("pg_reports.ui.errors.report_not_found")
|
|
72
72
|
return
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -138,7 +138,7 @@ module PgReports
|
|
|
138
138
|
report.send_to_telegram
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
render json: {success: true, message: "
|
|
141
|
+
render json: {success: true, message: I18n.t("pg_reports.ui.success.telegram_sent")}
|
|
142
142
|
rescue => e
|
|
143
143
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
144
144
|
end
|
|
@@ -177,7 +177,7 @@ module PgReports
|
|
|
177
177
|
query_params = params[:params] || {}
|
|
178
178
|
|
|
179
179
|
if query_hash.blank?
|
|
180
|
-
render json: {success: false, error: "
|
|
180
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.query_hash_required")}, status: :unprocessable_entity
|
|
181
181
|
return
|
|
182
182
|
end
|
|
183
183
|
|
|
@@ -185,7 +185,7 @@ module PgReports
|
|
|
185
185
|
unless PgReports.config.allow_raw_query_execution
|
|
186
186
|
render json: {
|
|
187
187
|
success: false,
|
|
188
|
-
error: "
|
|
188
|
+
error: I18n.t("pg_reports.ui.errors.query_execution_disabled")
|
|
189
189
|
}, status: :forbidden
|
|
190
190
|
return
|
|
191
191
|
end
|
|
@@ -195,11 +195,11 @@ module PgReports
|
|
|
195
195
|
query = retrieve_query_by_hash(query_hash)
|
|
196
196
|
|
|
197
197
|
if query.nil?
|
|
198
|
-
render json: {success: false, error: "
|
|
198
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.query_not_found_expired")}, status: :not_found
|
|
199
199
|
return
|
|
200
200
|
end
|
|
201
201
|
rescue SecurityError => e
|
|
202
|
-
render json: {success: false, error: "
|
|
202
|
+
render json: {success: false, error: "#{I18n.t("pg_reports.ui.errors.security_violation_prefix")} #{e.message}"}, status: :forbidden
|
|
203
203
|
return
|
|
204
204
|
end
|
|
205
205
|
|
|
@@ -207,7 +207,7 @@ module PgReports
|
|
|
207
207
|
if query.match?(/\b(NEW|OLD)\./i)
|
|
208
208
|
render json: {
|
|
209
209
|
success: false,
|
|
210
|
-
error: "
|
|
210
|
+
error: I18n.t("pg_reports.ui.errors.trigger_variables_not_allowed")
|
|
211
211
|
}, status: :unprocessable_entity
|
|
212
212
|
return
|
|
213
213
|
end
|
|
@@ -219,7 +219,7 @@ module PgReports
|
|
|
219
219
|
if final_query.match?(/\$\d+/)
|
|
220
220
|
render json: {
|
|
221
221
|
success: false,
|
|
222
|
-
error: "
|
|
222
|
+
error: I18n.t("pg_reports.ui.errors.missing_parameter_values")
|
|
223
223
|
}, status: :unprocessable_entity
|
|
224
224
|
return
|
|
225
225
|
end
|
|
@@ -248,7 +248,7 @@ module PgReports
|
|
|
248
248
|
query_params = params[:params] || {}
|
|
249
249
|
|
|
250
250
|
if query_hash.blank?
|
|
251
|
-
render json: {success: false, error: "
|
|
251
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.query_hash_required")}, status: :unprocessable_entity
|
|
252
252
|
return
|
|
253
253
|
end
|
|
254
254
|
|
|
@@ -256,7 +256,7 @@ module PgReports
|
|
|
256
256
|
unless PgReports.config.allow_raw_query_execution
|
|
257
257
|
render json: {
|
|
258
258
|
success: false,
|
|
259
|
-
error: "
|
|
259
|
+
error: I18n.t("pg_reports.ui.errors.query_execution_disabled")
|
|
260
260
|
}, status: :forbidden
|
|
261
261
|
return
|
|
262
262
|
end
|
|
@@ -266,11 +266,11 @@ module PgReports
|
|
|
266
266
|
query = retrieve_query_by_hash(query_hash)
|
|
267
267
|
|
|
268
268
|
if query.nil?
|
|
269
|
-
render json: {success: false, error: "
|
|
269
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.query_not_found_expired")}, status: :not_found
|
|
270
270
|
return
|
|
271
271
|
end
|
|
272
272
|
rescue SecurityError => e
|
|
273
|
-
render json: {success: false, error: "
|
|
273
|
+
render json: {success: false, error: "#{I18n.t("pg_reports.ui.errors.security_violation_prefix")} #{e.message}"}, status: :forbidden
|
|
274
274
|
return
|
|
275
275
|
end
|
|
276
276
|
|
|
@@ -281,7 +281,7 @@ module PgReports
|
|
|
281
281
|
if final_query.match?(/\$\d+/)
|
|
282
282
|
render json: {
|
|
283
283
|
success: false,
|
|
284
|
-
error: "
|
|
284
|
+
error: I18n.t("pg_reports.ui.errors.missing_parameter_values")
|
|
285
285
|
}, status: :unprocessable_entity
|
|
286
286
|
return
|
|
287
287
|
end
|
|
@@ -326,7 +326,7 @@ module PgReports
|
|
|
326
326
|
unless Rails.env.development?
|
|
327
327
|
render json: {
|
|
328
328
|
success: false,
|
|
329
|
-
error: "
|
|
329
|
+
error: I18n.t("pg_reports.ui.errors.migration_dev_only")
|
|
330
330
|
}, status: :forbidden
|
|
331
331
|
return
|
|
332
332
|
end
|
|
@@ -335,28 +335,28 @@ module PgReports
|
|
|
335
335
|
code = params[:code]
|
|
336
336
|
|
|
337
337
|
if file_name.blank? || code.blank?
|
|
338
|
-
render json: {success: false, error: "
|
|
338
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.filename_code_required")}, status: :unprocessable_entity
|
|
339
339
|
return
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
# Sanitize file name
|
|
343
343
|
safe_file_name = file_name.gsub(/[^a-z0-9_.]/, "")
|
|
344
344
|
unless safe_file_name.match?(/\A\d{14}_\w+\.rb\z/)
|
|
345
|
-
render json: {success: false, error: "
|
|
345
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.invalid_filename_format")}, status: :unprocessable_entity
|
|
346
346
|
return
|
|
347
347
|
end
|
|
348
348
|
|
|
349
349
|
# Find migrations directory
|
|
350
350
|
migrations_path = Rails.root.join("db", "migrate")
|
|
351
351
|
unless migrations_path.exist?
|
|
352
|
-
render json: {success: false, error: "
|
|
352
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.migrations_dir_not_found")}, status: :unprocessable_entity
|
|
353
353
|
return
|
|
354
354
|
end
|
|
355
355
|
|
|
356
356
|
file_path = migrations_path.join(safe_file_name)
|
|
357
357
|
File.write(file_path, code)
|
|
358
358
|
|
|
359
|
-
render json: {success: true, file_path: file_path.to_s, message: "
|
|
359
|
+
render json: {success: true, file_path: file_path.to_s, message: I18n.t("pg_reports.ui.success.migration_created")}
|
|
360
360
|
rescue => e
|
|
361
361
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
362
362
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="<%= I18n.locale %>">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<meta name="pg-reports-root" content="<%= request.script_name.presence || PgReports::Engine.routes.url_helpers.root_path %>">
|
|
7
|
-
<title
|
|
7
|
+
<title><%= t("pg_reports.ui.branding.page_title") %></title>
|
|
8
8
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%234f46e5'/%3E%3Crect x='5' y='18' width='5' height='9' rx='1' fill='%23fff'/%3E%3Crect x='13.5' y='12' width='5' height='15' rx='1' fill='%23fff'/%3E%3Crect x='22' y='6' width='5' height='21' rx='1' fill='%23fff'/%3E%3C/svg%3E">
|
|
9
9
|
<% if PgReports.config.load_external_fonts %>
|
|
10
10
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
@@ -78,15 +78,35 @@
|
|
|
78
78
|
font-size: 1rem;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
.logo-text {
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
gap: 5px;
|
|
85
|
+
line-height: 1.15;
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
.logo-text h1 {
|
|
82
89
|
font-size: 1.1rem;
|
|
83
90
|
font-weight: 600;
|
|
84
91
|
color: var(--text-primary);
|
|
92
|
+
line-height: 1.15;
|
|
93
|
+
margin-top: -3px;
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: baseline;
|
|
96
|
+
gap: 0.5rem;
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
.logo-
|
|
99
|
+
.logo-version {
|
|
88
100
|
font-size: 0.7rem;
|
|
101
|
+
font-weight: 500;
|
|
89
102
|
color: var(--text-muted);
|
|
103
|
+
letter-spacing: 0.02em;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.logo-subtitle {
|
|
107
|
+
font-size: 0.7rem;
|
|
108
|
+
color: var(--text-muted);
|
|
109
|
+
line-height: 1.15;
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
.header-badge {
|
|
@@ -487,6 +507,36 @@
|
|
|
487
507
|
width: 90%;
|
|
488
508
|
max-height: 90vh;
|
|
489
509
|
overflow: auto;
|
|
510
|
+
scrollbar-width: thin;
|
|
511
|
+
scrollbar-color: var(--border-color) var(--bg-secondary);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.modal-content::-webkit-scrollbar,
|
|
515
|
+
.modal-body::-webkit-scrollbar {
|
|
516
|
+
width: 8px;
|
|
517
|
+
height: 8px;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.modal-content::-webkit-scrollbar-track,
|
|
521
|
+
.modal-body::-webkit-scrollbar-track {
|
|
522
|
+
background: var(--bg-secondary);
|
|
523
|
+
border-radius: 4px;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.modal-content::-webkit-scrollbar-thumb,
|
|
527
|
+
.modal-body::-webkit-scrollbar-thumb {
|
|
528
|
+
background: var(--border-color);
|
|
529
|
+
border-radius: 4px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.modal-content::-webkit-scrollbar-thumb:hover,
|
|
533
|
+
.modal-body::-webkit-scrollbar-thumb:hover {
|
|
534
|
+
background: var(--text-muted);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.modal-content::-webkit-scrollbar-corner,
|
|
538
|
+
.modal-body::-webkit-scrollbar-corner {
|
|
539
|
+
background: var(--bg-secondary);
|
|
490
540
|
}
|
|
491
541
|
|
|
492
542
|
.modal-large {
|
|
@@ -954,6 +1004,13 @@
|
|
|
954
1004
|
|
|
955
1005
|
<script>
|
|
956
1006
|
const pgReportsRoot = document.querySelector('meta[name="pg-reports-root"]')?.content || '/pg_reports';
|
|
1007
|
+
window.PG_REPORTS_I18N = <%= raw I18n.t("pg_reports.ui").to_json %>;
|
|
1008
|
+
|
|
1009
|
+
// Format strings with %{var} placeholders
|
|
1010
|
+
window.pgReportsFormat = function(template, vars) {
|
|
1011
|
+
if (!template) return '';
|
|
1012
|
+
return template.replace(/%\{(\w+)\}/g, function(_, k) { return vars[k] != null ? vars[k] : ''; });
|
|
1013
|
+
};
|
|
957
1014
|
|
|
958
1015
|
function showToast(message, type = 'success') {
|
|
959
1016
|
const toast = document.getElementById('toast');
|
|
@@ -967,7 +1024,7 @@
|
|
|
967
1024
|
async function sendToTelegram(category, report, button) {
|
|
968
1025
|
if (button) {
|
|
969
1026
|
button.disabled = true;
|
|
970
|
-
button.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;vertical-align:middle;"></span>
|
|
1027
|
+
button.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;vertical-align:middle;"></span> ' + PG_REPORTS_I18N.actions.sending;
|
|
971
1028
|
}
|
|
972
1029
|
|
|
973
1030
|
try {
|
|
@@ -982,17 +1039,17 @@
|
|
|
982
1039
|
const data = await response.json();
|
|
983
1040
|
|
|
984
1041
|
if (data.success) {
|
|
985
|
-
showToast(
|
|
1042
|
+
showToast(PG_REPORTS_I18N.success.telegram_sent);
|
|
986
1043
|
} else {
|
|
987
|
-
showToast(data.error ||
|
|
1044
|
+
showToast(data.error || PG_REPORTS_I18N.errors.send_telegram_failed, 'error');
|
|
988
1045
|
}
|
|
989
1046
|
} catch (error) {
|
|
990
|
-
showToast(
|
|
1047
|
+
showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
|
|
991
1048
|
}
|
|
992
1049
|
|
|
993
1050
|
if (button) {
|
|
994
1051
|
button.disabled = false;
|
|
995
|
-
button.innerHTML =
|
|
1052
|
+
button.innerHTML = PG_REPORTS_I18N.actions.send_telegram;
|
|
996
1053
|
}
|
|
997
1054
|
}
|
|
998
1055
|
</script>
|
|
@@ -2,39 +2,39 @@
|
|
|
2
2
|
<div id="ide-settings-modal" class="modal" style="display: none;">
|
|
3
3
|
<div class="modal-content modal-small">
|
|
4
4
|
<div class="modal-header">
|
|
5
|
-
<h3
|
|
5
|
+
<h3><%= t("pg_reports.ui.modals.ide_settings_title") %></h3>
|
|
6
6
|
<button class="modal-close" onclick="closeIdeSettingsModal()">×</button>
|
|
7
7
|
</div>
|
|
8
8
|
<div class="modal-body">
|
|
9
|
-
<p class="settings-label"
|
|
9
|
+
<p class="settings-label"><%= t("pg_reports.ui.settings.default_ide_label") %></p>
|
|
10
10
|
<div class="ide-options">
|
|
11
11
|
<label class="ide-option">
|
|
12
12
|
<input type="radio" name="default-ide" value="" onchange="setDefaultIde('')">
|
|
13
|
-
<span
|
|
13
|
+
<span><%= t("pg_reports.ui.settings.ide_show_menu") %></span>
|
|
14
14
|
</label>
|
|
15
15
|
<label class="ide-option">
|
|
16
16
|
<input type="radio" name="default-ide" value="vscode-wsl" onchange="setDefaultIde('vscode-wsl')">
|
|
17
|
-
<span
|
|
17
|
+
<span><%= t("pg_reports.ui.settings.ide_vscode_wsl") %></span>
|
|
18
18
|
</label>
|
|
19
19
|
<label class="ide-option">
|
|
20
20
|
<input type="radio" name="default-ide" value="vscode" onchange="setDefaultIde('vscode')">
|
|
21
|
-
<span
|
|
21
|
+
<span><%= t("pg_reports.ui.settings.ide_vscode") %></span>
|
|
22
22
|
</label>
|
|
23
23
|
<label class="ide-option">
|
|
24
24
|
<input type="radio" name="default-ide" value="rubymine" onchange="setDefaultIde('rubymine')">
|
|
25
|
-
<span
|
|
25
|
+
<span><%= t("pg_reports.ui.settings.ide_rubymine") %></span>
|
|
26
26
|
</label>
|
|
27
27
|
<label class="ide-option">
|
|
28
28
|
<input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
|
|
29
|
-
<span
|
|
29
|
+
<span><%= t("pg_reports.ui.settings.ide_intellij") %></span>
|
|
30
30
|
</label>
|
|
31
31
|
<label class="ide-option">
|
|
32
32
|
<input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
|
|
33
|
-
<span
|
|
33
|
+
<span><%= t("pg_reports.ui.settings.ide_cursor_wsl") %></span>
|
|
34
34
|
</label>
|
|
35
35
|
<label class="ide-option">
|
|
36
36
|
<input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
|
|
37
|
-
<span
|
|
37
|
+
<span><%= t("pg_reports.ui.settings.ide_cursor") %></span>
|
|
38
38
|
</label>
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<div id="problem-modal" class="problem-modal" style="display: none;">
|
|
46
46
|
<div class="problem-modal-content">
|
|
47
47
|
<div class="problem-modal-header">
|
|
48
|
-
<h3
|
|
48
|
+
<h3><%= t("pg_reports.ui.modals.problem_detected_title") %></h3>
|
|
49
49
|
<button class="modal-close" onclick="closeProblemModal()">×</button>
|
|
50
50
|
</div>
|
|
51
51
|
<div class="problem-modal-body" id="problem-modal-body">
|
|
@@ -58,24 +58,24 @@
|
|
|
58
58
|
<div id="explain-modal" class="modal" style="display: none;">
|
|
59
59
|
<div class="modal-content modal-wide">
|
|
60
60
|
<div class="modal-header">
|
|
61
|
-
<h3
|
|
61
|
+
<h3><%= t("pg_reports.ui.modals.query_analyzer_title") %></h3>
|
|
62
62
|
<button class="modal-close" onclick="closeExplainModal()">×</button>
|
|
63
63
|
</div>
|
|
64
64
|
<div class="modal-body" id="explain-modal-body">
|
|
65
65
|
<div class="explain-query-section">
|
|
66
|
-
<label class="explain-label"
|
|
66
|
+
<label class="explain-label"><%= t("pg_reports.ui.modals.query_label") %></label>
|
|
67
67
|
<pre class="explain-query" id="explain-query-display"></pre>
|
|
68
68
|
</div>
|
|
69
69
|
<div id="explain-params-section" class="explain-params-section" style="display: none;">
|
|
70
|
-
<label class="explain-label"
|
|
70
|
+
<label class="explain-label"><%= t("pg_reports.ui.modals.parameters_label") %></label>
|
|
71
71
|
<div id="explain-params-inputs"></div>
|
|
72
72
|
</div>
|
|
73
73
|
<div class="explain-actions">
|
|
74
74
|
<button class="btn btn-secondary" onclick="executeExplainAnalyze()" id="btn-explain">
|
|
75
|
-
|
|
75
|
+
<%= t("pg_reports.ui.actions.explain_analyze") %>
|
|
76
76
|
</button>
|
|
77
77
|
<button class="btn btn-secondary" onclick="executeQuery()" id="btn-execute">
|
|
78
|
-
|
|
78
|
+
<%= t("pg_reports.ui.actions.execute_query") %>
|
|
79
79
|
</button>
|
|
80
80
|
</div>
|
|
81
81
|
<div id="explain-loading" class="loading" style="display: none;">
|
|
@@ -90,22 +90,22 @@
|
|
|
90
90
|
<div id="migration-modal" class="modal" style="display: none;">
|
|
91
91
|
<div class="modal-content">
|
|
92
92
|
<div class="modal-header">
|
|
93
|
-
<h3
|
|
93
|
+
<h3><%= t("pg_reports.ui.modals.migration_title") %></h3>
|
|
94
94
|
<button class="modal-close" onclick="closeMigrationModal()">×</button>
|
|
95
95
|
</div>
|
|
96
96
|
<div class="modal-body" id="migration-modal-body">
|
|
97
97
|
<div class="migration-warning" style="background: rgba(255, 152, 0, 0.1); border: 1px solid rgba(255, 152, 0, 0.3); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
98
|
-
<p style="margin: 0 0 0.5rem 0; font-weight: 600; color: #ffb74d;"
|
|
98
|
+
<p style="margin: 0 0 0.5rem 0; font-weight: 600; color: #ffb74d;"><%= t("pg_reports.ui.documentation.threshold_warning_label").chomp(":") %></p>
|
|
99
99
|
<p style="margin: 0; color: #ffcc80; font-size: 0.875rem; line-height: 1.5;">
|
|
100
|
-
|
|
101
|
-
<strong
|
|
100
|
+
<%= t("pg_reports.ui.modals.migration_warning") %>
|
|
101
|
+
<strong><%= t("pg_reports.ui.modals.migration_warning_dev_only") %></strong>
|
|
102
102
|
</p>
|
|
103
103
|
</div>
|
|
104
|
-
<p class="settings-label"
|
|
104
|
+
<p class="settings-label"><%= t("pg_reports.ui.modals.migration_subtitle") %></p>
|
|
105
105
|
<div id="migration-code" class="migration-code"></div>
|
|
106
106
|
<div class="migration-actions">
|
|
107
|
-
<button class="btn btn-secondary" onclick="copyMigrationCode()"
|
|
108
|
-
<button class="btn btn-primary" id="create-migration-btn" onclick="createMigrationFile()"
|
|
107
|
+
<button class="btn btn-secondary" onclick="copyMigrationCode()"><%= t("pg_reports.ui.actions.copy_code") %></button>
|
|
108
|
+
<button class="btn btn-primary" id="create-migration-btn" onclick="createMigrationFile()"><%= t("pg_reports.ui.actions.create_migration_file") %></button>
|
|
109
109
|
</div>
|
|
110
110
|
</div>
|
|
111
111
|
</div>
|