pg_reports 0.8.2 → 0.9.1
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 +48 -0
- data/app/controllers/pg_reports/dashboard_controller.rb +104 -6
- data/app/views/layouts/pg_reports/application.html.erb +712 -214
- data/app/views/pg_reports/dashboard/_database_selector.html.erb +2 -1
- data/app/views/pg_reports/dashboard/_settings_modal.html.erb +55 -0
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +11 -49
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +101 -90
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +392 -476
- data/app/views/pg_reports/dashboard/index.html.erb +217 -493
- data/app/views/pg_reports/dashboard/show.html.erb +13 -11
- data/config/brakeman.ignore +24 -1
- data/config/locales/en.yml +58 -50
- data/config/locales/ru.yml +58 -50
- data/config/locales/uk.yml +58 -50
- data/config/routes.rb +1 -0
- data/lib/pg_reports/connection/error_translator.rb +43 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +10 -0
- data/lib/pg_reports/explain_analyzer.rb +0 -3
- data/lib/pg_reports/query_monitor.rb +15 -3
- data/lib/pg_reports/version.rb +1 -1
- metadata +2 -1
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
</span>
|
|
35
35
|
<% elsif @database_error %>
|
|
36
36
|
<span class="db-selector-error" title="<%= @database_error[:detail] %>">
|
|
37
|
-
|
|
37
|
+
<svg class="icon" aria-hidden="true"><use href="#i-alert"></use></svg>
|
|
38
|
+
<%= @database_error[:title] %>
|
|
38
39
|
</span>
|
|
39
40
|
<% end %>
|
|
@@ -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;">
|
|
@@ -71,7 +30,7 @@
|
|
|
71
30
|
<div id="explain-params-inputs"></div>
|
|
72
31
|
</div>
|
|
73
32
|
<div class="explain-actions">
|
|
74
|
-
<button class="btn btn-
|
|
33
|
+
<button class="btn btn-primary" onclick="executeExplainAnalyze()" id="btn-explain">
|
|
75
34
|
<%= t("pg_reports.ui.actions.explain_analyze") %>
|
|
76
35
|
</button>
|
|
77
36
|
<button class="btn btn-secondary" onclick="executeQuery()" id="btn-execute">
|
|
@@ -94,12 +53,15 @@
|
|
|
94
53
|
<button class="modal-close" onclick="closeMigrationModal()">×</button>
|
|
95
54
|
</div>
|
|
96
55
|
<div class="modal-body" id="migration-modal-body">
|
|
97
|
-
<div class="
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
<
|
|
102
|
-
|
|
56
|
+
<div class="callout callout-warning">
|
|
57
|
+
<svg class="icon" aria-hidden="true"><use href="#i-alert"></use></svg>
|
|
58
|
+
<div>
|
|
59
|
+
<strong><%= t("pg_reports.ui.documentation.threshold_warning_label").chomp(":") %></strong>
|
|
60
|
+
<p>
|
|
61
|
+
<%= t("pg_reports.ui.modals.migration_warning") %>
|
|
62
|
+
<strong><%= t("pg_reports.ui.modals.migration_warning_dev_only") %></strong>
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
103
65
|
</div>
|
|
104
66
|
<p class="settings-label"><%= t("pg_reports.ui.modals.migration_subtitle") %></p>
|
|
105
67
|
<div id="migration-code" class="migration-code"></div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
const svgIcon = (name, cls) => `<svg class="icon ${cls || ''}"><use href="#i-${name}"></use></svg>`;
|
|
2
3
|
let currentReportData = null;
|
|
3
4
|
let currentProblemExplanations = {};
|
|
4
5
|
const category = '<%= @category %>';
|
|
@@ -18,8 +19,10 @@
|
|
|
18
19
|
|
|
19
20
|
if (!topScrollWrapper || !topScrollContent || !tableWrapper || !table) return;
|
|
20
21
|
|
|
21
|
-
// Check if table overflows
|
|
22
|
-
|
|
22
|
+
// Check if table overflows. On touch there is no visible scrollbar to
|
|
23
|
+
// drag, so the proxy would only be an empty strip above the table.
|
|
24
|
+
const coarsePointer = window.matchMedia('(pointer: coarse)').matches;
|
|
25
|
+
if (table.scrollWidth > tableWrapper.clientWidth && !coarsePointer) {
|
|
23
26
|
topScrollWrapper.style.display = 'block';
|
|
24
27
|
topScrollContent.style.width = table.scrollWidth + 'px';
|
|
25
28
|
|
|
@@ -126,7 +129,7 @@
|
|
|
126
129
|
if (problemInfo && problemInfo.level) {
|
|
127
130
|
rowClass += problemInfo.level === 'critical' ? ' critical-row' : ' warning-row';
|
|
128
131
|
const indicatorClass = problemInfo.level === 'critical' ? 'critical' : 'warning';
|
|
129
|
-
const indicatorIcon =
|
|
132
|
+
const indicatorIcon = svgIcon('alert');
|
|
130
133
|
problemIndicator = `<span class="problem-indicator ${indicatorClass}" onclick="event.stopPropagation(); showProblemModal(${JSON.stringify(problemInfo.problems).replace(/"/g, '"')}, ${JSON.stringify(row).replace(/"/g, '"')})">${indicatorIcon}</span>`;
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -157,6 +160,8 @@
|
|
|
157
160
|
|
|
158
161
|
tableBody.innerHTML = rowsHtml;
|
|
159
162
|
|
|
163
|
+
markClippedSources(tableBody);
|
|
164
|
+
|
|
160
165
|
// Re-setup top scrollbar
|
|
161
166
|
setTimeout(setupTopScrollbar, 0);
|
|
162
167
|
}
|
|
@@ -409,11 +414,16 @@
|
|
|
409
414
|
let lineNumber = null;
|
|
410
415
|
let methodName = null;
|
|
411
416
|
|
|
412
|
-
// Try to match file:line pattern
|
|
413
|
-
|
|
417
|
+
// Try to match file:line pattern. Deliberately no extension allow-list —
|
|
418
|
+
// source locations legitimately end in .rake, .jbuilder, .ru, .haml, or in
|
|
419
|
+
// no extension at all (Rakefile:5), and anything the list missed silently
|
|
420
|
+
// lost its shortening and its IDE link. Lazy up to the first ":<line>" that
|
|
421
|
+
// ends the token, so a trailing ":in `method'" or a Windows drive letter
|
|
422
|
+
// ("C:\app\x.rb:12") doesn't derail it.
|
|
423
|
+
const fileLineMatch = source.match(/^(.+?):(\d+)(?=$|[:\s])/);
|
|
414
424
|
if (fileLineMatch) {
|
|
415
425
|
filePath = fileLineMatch[1];
|
|
416
|
-
lineNumber = parseInt(fileLineMatch[
|
|
426
|
+
lineNumber = parseInt(fileLineMatch[2], 10);
|
|
417
427
|
}
|
|
418
428
|
|
|
419
429
|
// Try to match Controller#action pattern (e.g., PostsController#index)
|
|
@@ -449,6 +459,70 @@
|
|
|
449
459
|
const railsRootPath = '<%= Rails.root.to_s %>';
|
|
450
460
|
const wslDistro = 'Ubuntu';
|
|
451
461
|
|
|
462
|
+
// Prefix to strip when displaying a source path. Only meaningful when the
|
|
463
|
+
// engine is mounted in a host app — in standalone mode Rails.root is a
|
|
464
|
+
// throwaway tmpdir that no query's source location will ever sit under.
|
|
465
|
+
const sourceRootPath = <%== (PgReports.config.standalone ? nil : Rails.root.to_s).to_json %>;
|
|
466
|
+
|
|
467
|
+
// The badge shows the shortest path that still identifies the line. The full
|
|
468
|
+
// value stays in the tooltip, the expanded row and the exports; IDE links are
|
|
469
|
+
// built from the unshortened path.
|
|
470
|
+
function shortenSourcePath(filePath) {
|
|
471
|
+
if (!filePath) return filePath;
|
|
472
|
+
|
|
473
|
+
if (sourceRootPath && filePath.startsWith(sourceRootPath + '/')) {
|
|
474
|
+
return filePath.slice(sourceRootPath.length + 1);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// A bundled gem: everything before the gem's own directory is install noise.
|
|
478
|
+
// The greedy prefix is deliberate — a gem home nests "/gems/" twice
|
|
479
|
+
// ("…/lib/ruby/gems/3.4.0/gems/activerecord-8.1.3/…") and only the last one
|
|
480
|
+
// starts the gem's own path.
|
|
481
|
+
const gem = filePath.match(/^.*\/gems\/([^/]+\/.+)$/);
|
|
482
|
+
if (gem) return gem[1];
|
|
483
|
+
|
|
484
|
+
return filePath;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Split so the directory can be clipped while the file name and line — the part
|
|
488
|
+
// you actually navigate by — stay readable. The directory is clipped at its
|
|
489
|
+
// *start*: see .source-dir in the styles, and markClippedSources below for the
|
|
490
|
+
// leading ellipsis.
|
|
491
|
+
function sourceBadgeLabel(parsed) {
|
|
492
|
+
if (!parsed.filePath) {
|
|
493
|
+
return `<span class="source-file"><span>${escapeHtml(parsed.original)}</span></span>`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const shortened = shortenSourcePath(parsed.filePath);
|
|
497
|
+
const line = parsed.lineNumber ? ':' + parsed.lineNumber : '';
|
|
498
|
+
const cut = shortened.lastIndexOf('/');
|
|
499
|
+
|
|
500
|
+
const ellipsis = '<span class="source-ellipsis" aria-hidden="true">…</span>';
|
|
501
|
+
const file = (text) => `<span class="source-file"><span>${escapeHtml(text)}</span></span>`;
|
|
502
|
+
|
|
503
|
+
if (cut < 0) return ellipsis + file(shortened + line);
|
|
504
|
+
|
|
505
|
+
return ellipsis +
|
|
506
|
+
`<span class="source-dir"><span>${escapeHtml(shortened.slice(0, cut))}</span></span>` +
|
|
507
|
+
file(shortened.slice(cut) + line);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Whether a part actually overflows can only be known after layout, and the
|
|
511
|
+
// leading ellipsis must not appear when nothing is cut. Measured on the inner
|
|
512
|
+
// span, not on its container: the overflow here runs off the *start* edge, and
|
|
513
|
+
// scrollWidth only ever reports overflow past the end edge.
|
|
514
|
+
function markClippedSources(root) {
|
|
515
|
+
(root || document).querySelectorAll('.source-badge').forEach(badge => {
|
|
516
|
+
const parts = badge.querySelectorAll('.source-dir, .source-file');
|
|
517
|
+
const clipped = Array.prototype.some.call(parts, part => {
|
|
518
|
+
const inner = part.firstElementChild;
|
|
519
|
+
return inner && inner.getBoundingClientRect().width > part.clientWidth + 1;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
badge.classList.toggle('is-clipped', clipped);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
452
526
|
// Generate IDE URLs
|
|
453
527
|
function generateIdeUrls(filePath, lineNumber) {
|
|
454
528
|
if (!filePath) return [];
|
|
@@ -521,7 +595,7 @@
|
|
|
521
595
|
const ideUrls = generateIdeUrls(parsed.filePath, parsed.lineNumber);
|
|
522
596
|
|
|
523
597
|
if (ideUrls.length === 0) {
|
|
524
|
-
return `<span class="source-badge" title="${
|
|
598
|
+
return `<span class="source-badge" title="${escapeHtmlAttr(parsed.original)}">${sourceBadgeLabel(parsed)}</span>`;
|
|
525
599
|
}
|
|
526
600
|
|
|
527
601
|
// Check if user has a default IDE set
|
|
@@ -529,7 +603,7 @@
|
|
|
529
603
|
if (defaultIde && ideKeyMap[defaultIde] !== undefined) {
|
|
530
604
|
const ideUrl = ideUrls[ideKeyMap[defaultIde]];
|
|
531
605
|
if (ideUrl) {
|
|
532
|
-
return `<a class="source-badge clickable" href="${ideUrl.url}" onclick="event.stopPropagation();" title="${
|
|
606
|
+
return `<a class="source-badge clickable" href="${ideUrl.url}" onclick="event.stopPropagation();" title="${escapeHtmlAttr(parsed.original)}">${sourceBadgeLabel(parsed)}</a>`;
|
|
533
607
|
}
|
|
534
608
|
}
|
|
535
609
|
|
|
@@ -538,7 +612,7 @@
|
|
|
538
612
|
|
|
539
613
|
let dropdownHtml = `
|
|
540
614
|
<div class="ide-dropdown">
|
|
541
|
-
<span class="source-badge clickable" data-dropdown-id="${dropdownId}" title="${
|
|
615
|
+
<span class="source-badge clickable" data-dropdown-id="${dropdownId}" title="${escapeHtmlAttr(parsed.original)}">${sourceBadgeLabel(parsed)}</span>
|
|
542
616
|
<div class="ide-dropdown-menu" id="${dropdownId}">
|
|
543
617
|
`;
|
|
544
618
|
|
|
@@ -642,7 +716,7 @@
|
|
|
642
716
|
const rowJson = JSON.stringify(row).replace(/"/g, '"');
|
|
643
717
|
|
|
644
718
|
html += `<div class="detail-actions">`;
|
|
645
|
-
html += `<button class="btn-save ${isSaved ? 'saved' : ''}" onclick="event.stopPropagation(); toggleSaveRecord('${rowId}', ${rowJson}, this)">${isSaved ?
|
|
719
|
+
html += `<button class="btn-save ${isSaved ? 'saved' : ''}" onclick="event.stopPropagation(); toggleSaveRecord('${rowId}', ${rowJson}, this)">${svgIcon('pin')}${isSaved ? PG_REPORTS_I18N.actions.saved_marker : PG_REPORTS_I18N.actions.save_for_comparison}</button>`;
|
|
646
720
|
|
|
647
721
|
if (hasQuery) {
|
|
648
722
|
// Only show EXPLAIN ANALYZE for SELECT queries
|
|
@@ -651,7 +725,7 @@
|
|
|
651
725
|
// Use data attribute to pass query hash for security
|
|
652
726
|
const queryBase64 = btoa(unescape(encodeURIComponent(row.query)));
|
|
653
727
|
const queryHash = row.query_hash || '';
|
|
654
|
-
html += `<button class="btn-explain" data-query-b64="${queryBase64}" data-query-hash="${queryHash}" onclick="event.stopPropagation(); runExplainAnalyzeFromButton(this)"
|
|
728
|
+
html += `<button class="btn-explain" data-query-b64="${queryBase64}" data-query-hash="${queryHash}" onclick="event.stopPropagation(); runExplainAnalyzeFromButton(this)">${svgIcon('activity')}EXPLAIN ANALYZE</button>`;
|
|
655
729
|
}
|
|
656
730
|
}
|
|
657
731
|
|
|
@@ -661,9 +735,9 @@
|
|
|
661
735
|
const tableName = row.table_name || row.tablename || '';
|
|
662
736
|
const schemaName = row.schema_name || row.schemaname || 'public';
|
|
663
737
|
if (isDevelopment) {
|
|
664
|
-
html += `<button class="btn-migration" onclick="event.stopPropagation(); showMigrationModal('${escapeHtml(indexName)}', '${escapeHtml(tableName)}', '${escapeHtml(schemaName)}')"
|
|
738
|
+
html += `<button class="btn-migration" onclick="event.stopPropagation(); showMigrationModal('${escapeHtml(indexName)}', '${escapeHtml(tableName)}', '${escapeHtml(schemaName)}')">${svgIcon('trash')}Generate Migration</button>`;
|
|
665
739
|
} else {
|
|
666
|
-
html += `<button class="btn-migration" disabled title="Migration generation is only available in development environment"
|
|
740
|
+
html += `<button class="btn-migration" disabled title="Migration generation is only available in development environment" >${svgIcon('trash')}Generate Migration (Development Only)</button>`;
|
|
667
741
|
}
|
|
668
742
|
}
|
|
669
743
|
|
|
@@ -780,7 +854,7 @@
|
|
|
780
854
|
document.addEventListener('keydown', function(e) {
|
|
781
855
|
if (e.key === 'Escape') {
|
|
782
856
|
closeProblemModal();
|
|
783
|
-
|
|
857
|
+
closeSettingsModal();
|
|
784
858
|
closeExplainModal();
|
|
785
859
|
closeMigrationModal();
|
|
786
860
|
}
|
|
@@ -793,44 +867,6 @@
|
|
|
793
867
|
}
|
|
794
868
|
});
|
|
795
869
|
|
|
796
|
-
// IDE Settings functions
|
|
797
|
-
function showIdeSettingsModal() {
|
|
798
|
-
const modal = document.getElementById('ide-settings-modal');
|
|
799
|
-
modal.style.display = 'flex';
|
|
800
|
-
loadIdeSettingsState();
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
function closeIdeSettingsModal() {
|
|
804
|
-
document.getElementById('ide-settings-modal').style.display = 'none';
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function setDefaultIde(ideKey) {
|
|
808
|
-
if (ideKey) {
|
|
809
|
-
localStorage.setItem('pgReportsDefaultIde', ideKey);
|
|
810
|
-
} else {
|
|
811
|
-
localStorage.removeItem('pgReportsDefaultIde');
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
function getDefaultIde() {
|
|
816
|
-
return localStorage.getItem('pgReportsDefaultIde') || '';
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function loadIdeSettingsState() {
|
|
820
|
-
const currentIde = getDefaultIde();
|
|
821
|
-
const radios = document.querySelectorAll('input[name="default-ide"]');
|
|
822
|
-
radios.forEach(radio => {
|
|
823
|
-
radio.checked = (radio.value === currentIde);
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// Close IDE settings modal on backdrop click
|
|
828
|
-
document.getElementById('ide-settings-modal')?.addEventListener('click', function(e) {
|
|
829
|
-
if (e.target === this) {
|
|
830
|
-
closeIdeSettingsModal();
|
|
831
|
-
}
|
|
832
|
-
});
|
|
833
|
-
|
|
834
870
|
// ==========================================
|
|
835
871
|
// SAVED RECORDS FUNCTIONALITY
|
|
836
872
|
// ==========================================
|
|
@@ -1158,15 +1194,15 @@
|
|
|
1158
1194
|
if (data.summary && data.summary.total_problems > 0) {
|
|
1159
1195
|
html += '<div class="explain-summary explain-summary-' + data.summary.status + '">';
|
|
1160
1196
|
html += '<div class="explain-summary-header">';
|
|
1161
|
-
html += '
|
|
1197
|
+
html += svgIcon(data.summary.status === 'good' ? 'check' : 'alert', 'explain-summary-icon');
|
|
1162
1198
|
html += '<span class="explain-summary-title">' + escapeHtml(data.summary.status_text) + '</span>';
|
|
1163
1199
|
html += '</div>';
|
|
1164
1200
|
html += '<div class="explain-summary-stats">';
|
|
1165
1201
|
if (data.summary.critical_problems > 0) {
|
|
1166
|
-
html += '<span class="explain-summary-stat critical"
|
|
1202
|
+
html += '<span class="explain-summary-stat critical">' + svgIcon('alert') + data.summary.critical_problems + ' critical</span>';
|
|
1167
1203
|
}
|
|
1168
1204
|
if (data.summary.warnings > 0) {
|
|
1169
|
-
html += '<span class="explain-summary-stat warning"
|
|
1205
|
+
html += '<span class="explain-summary-stat warning">' + svgIcon('alert') + data.summary.warnings + ' warnings</span>';
|
|
1170
1206
|
}
|
|
1171
1207
|
html += '</div>';
|
|
1172
1208
|
html += '</div>';
|
|
@@ -1212,7 +1248,7 @@
|
|
|
1212
1248
|
|
|
1213
1249
|
data.problems.forEach(problem => {
|
|
1214
1250
|
const severityClass = 'problem-' + problem.severity;
|
|
1215
|
-
const severityIcon =
|
|
1251
|
+
const severityIcon = svgIcon(problem.severity === 'info' ? 'info' : 'alert');
|
|
1216
1252
|
|
|
1217
1253
|
html += '<div class="explain-problem ' + severityClass + '">';
|
|
1218
1254
|
html += '<div class="explain-problem-header">';
|
|
@@ -1226,7 +1262,7 @@
|
|
|
1226
1262
|
html += '<div class="explain-problem-details">' + escapeHtml(problem.details) + '</div>';
|
|
1227
1263
|
}
|
|
1228
1264
|
if (problem.recommendation) {
|
|
1229
|
-
html += '<div class="explain-problem-recommendation"
|
|
1265
|
+
html += '<div class="explain-problem-recommendation">' + svgIcon('bulb') + escapeHtml(problem.recommendation) + '</div>';
|
|
1230
1266
|
}
|
|
1231
1267
|
html += '</div>';
|
|
1232
1268
|
});
|
|
@@ -1301,7 +1337,7 @@
|
|
|
1301
1337
|
|
|
1302
1338
|
// Problem indicator
|
|
1303
1339
|
if (lineProblem) {
|
|
1304
|
-
const problemIcon =
|
|
1340
|
+
const problemIcon = svgIcon('alert');
|
|
1305
1341
|
html += '<span class="explain-line-problem-indicator" title="' + escapeHtml(lineProblem.message) + '">' + problemIcon + '</span>';
|
|
1306
1342
|
}
|
|
1307
1343
|
|
|
@@ -1338,15 +1374,7 @@
|
|
|
1338
1374
|
// Security check
|
|
1339
1375
|
if (!allowRawQueryExecution) {
|
|
1340
1376
|
showToast(PG_REPORTS_I18N.errors.explain_disabled_toast, 'error');
|
|
1341
|
-
content.innerHTML =
|
|
1342
|
-
<strong>${PG_REPORTS_I18N.modals.query_execution_disabled_title}</strong><br><br>
|
|
1343
|
-
${PG_REPORTS_I18N.modals.query_execution_disabled_intro}<br>
|
|
1344
|
-
<code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
|
|
1345
|
-
PgReports.configure do |config|<br>
|
|
1346
|
-
config.allow_raw_query_execution = true<br>
|
|
1347
|
-
end
|
|
1348
|
-
</code>
|
|
1349
|
-
</div>`;
|
|
1377
|
+
content.innerHTML = pgReportsDisabledNotice(PG_REPORTS_I18N.modals.query_execution_disabled_title);
|
|
1350
1378
|
return;
|
|
1351
1379
|
}
|
|
1352
1380
|
|
|
@@ -1387,15 +1415,7 @@ end
|
|
|
1387
1415
|
// Security check
|
|
1388
1416
|
if (!allowRawQueryExecution) {
|
|
1389
1417
|
showToast(PG_REPORTS_I18N.errors.execute_disabled_toast, 'error');
|
|
1390
|
-
content.innerHTML =
|
|
1391
|
-
<strong>${PG_REPORTS_I18N.modals.query_execution_disabled_title}</strong><br><br>
|
|
1392
|
-
${PG_REPORTS_I18N.modals.query_execution_disabled_intro}<br>
|
|
1393
|
-
<code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
|
|
1394
|
-
PgReports.configure do |config|<br>
|
|
1395
|
-
config.allow_raw_query_execution = true<br>
|
|
1396
|
-
end
|
|
1397
|
-
</code>
|
|
1398
|
-
</div>`;
|
|
1418
|
+
content.innerHTML = pgReportsDisabledNotice(PG_REPORTS_I18N.modals.query_execution_disabled_title);
|
|
1399
1419
|
return;
|
|
1400
1420
|
}
|
|
1401
1421
|
|
|
@@ -1552,24 +1572,15 @@ end
|
|
|
1552
1572
|
|
|
1553
1573
|
// Security check
|
|
1554
1574
|
if (!allowRawQueryExecution) {
|
|
1555
|
-
const message =
|
|
1575
|
+
const message = PG_REPORTS_I18N.errors.migration_disabled_toast;
|
|
1556
1576
|
showToast(message, 'error');
|
|
1557
1577
|
|
|
1558
1578
|
const modalBody = document.getElementById('migration-modal-body');
|
|
1559
1579
|
if (modalBody) {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
<strong>⚠️ Migration creation is disabled</strong><br><br>
|
|
1565
|
-
To enable this feature, add to your configuration:<br>
|
|
1566
|
-
<code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
|
|
1567
|
-
PgReports.configure do |config|<br>
|
|
1568
|
-
config.allow_raw_query_execution = true<br>
|
|
1569
|
-
end
|
|
1570
|
-
</code>
|
|
1571
|
-
`;
|
|
1572
|
-
modalBody.appendChild(errorDiv);
|
|
1580
|
+
modalBody.insertAdjacentHTML(
|
|
1581
|
+
'beforeend',
|
|
1582
|
+
pgReportsDisabledNotice(PG_REPORTS_I18N.modals.migration_disabled_title)
|
|
1583
|
+
);
|
|
1573
1584
|
}
|
|
1574
1585
|
return;
|
|
1575
1586
|
}
|