pg_reports 0.4.0 → 0.5.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 +104 -0
- data/README.md +129 -4
- data/app/controllers/pg_reports/dashboard_controller.rb +188 -25
- data/app/views/layouts/pg_reports/application.html.erb +282 -0
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +184 -23
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +373 -0
- data/app/views/pg_reports/dashboard/index.html.erb +419 -0
- data/config/locales/en.yml +45 -0
- data/config/locales/ru.yml +45 -0
- data/config/routes.rb +8 -0
- data/lib/pg_reports/configuration.rb +13 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +24 -1
- data/lib/pg_reports/definitions/connections/connection_churn.yml +49 -0
- data/lib/pg_reports/definitions/connections/pool_saturation.yml +42 -0
- data/lib/pg_reports/definitions/connections/pool_usage.yml +43 -0
- data/lib/pg_reports/definitions/connections/pool_wait_times.yml +44 -0
- data/lib/pg_reports/definitions/queries/missing_index_queries.yml +3 -3
- data/lib/pg_reports/explain_analyzer.rb +338 -0
- data/lib/pg_reports/modules/schema_analysis.rb +4 -6
- data/lib/pg_reports/modules/system.rb +19 -2
- data/lib/pg_reports/query_monitor.rb +280 -0
- data/lib/pg_reports/sql/connections/connection_churn.sql +37 -0
- data/lib/pg_reports/sql/connections/pool_saturation.sql +90 -0
- data/lib/pg_reports/sql/connections/pool_usage.sql +31 -0
- data/lib/pg_reports/sql/connections/pool_wait_times.sql +19 -0
- data/lib/pg_reports/sql/queries/all_queries.sql +17 -15
- data/lib/pg_reports/sql/queries/expensive_queries.sql +9 -4
- data/lib/pg_reports/sql/queries/heavy_queries.sql +14 -12
- data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +16 -14
- data/lib/pg_reports/sql/queries/missing_index_queries.sql +18 -16
- data/lib/pg_reports/sql/queries/slow_queries.sql +14 -12
- data/lib/pg_reports/sql/system/databases_list.sql +8 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +2 -0
- metadata +56 -3
|
@@ -540,6 +540,288 @@
|
|
|
540
540
|
justify-content: center;
|
|
541
541
|
}
|
|
542
542
|
}
|
|
543
|
+
|
|
544
|
+
/* ============================================
|
|
545
|
+
Query Monitoring Panel
|
|
546
|
+
============================================ */
|
|
547
|
+
.query-monitoring-panel {
|
|
548
|
+
margin-bottom: 2rem;
|
|
549
|
+
background: var(--bg-card);
|
|
550
|
+
border: 1px solid var(--border-color);
|
|
551
|
+
border-radius: 16px;
|
|
552
|
+
padding: 1.25rem 1.5rem;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.query-monitoring-header {
|
|
556
|
+
display: flex;
|
|
557
|
+
align-items: center;
|
|
558
|
+
justify-content: space-between;
|
|
559
|
+
margin-bottom: 1.25rem;
|
|
560
|
+
padding-bottom: 1rem;
|
|
561
|
+
border-bottom: 1px solid var(--border-color);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.query-monitoring-title {
|
|
565
|
+
display: flex;
|
|
566
|
+
align-items: center;
|
|
567
|
+
gap: 0.75rem;
|
|
568
|
+
font-weight: 600;
|
|
569
|
+
font-size: 1rem;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.monitoring-indicator {
|
|
573
|
+
width: 8px;
|
|
574
|
+
height: 8px;
|
|
575
|
+
border-radius: 50%;
|
|
576
|
+
background: var(--text-muted);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.monitoring-indicator.active {
|
|
580
|
+
background: var(--accent-rose);
|
|
581
|
+
animation: pulse 2s infinite;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
@keyframes pulse {
|
|
585
|
+
0%, 100% {
|
|
586
|
+
opacity: 1;
|
|
587
|
+
}
|
|
588
|
+
50% {
|
|
589
|
+
opacity: 0.5;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.session-badge {
|
|
594
|
+
margin-left: 0.5rem;
|
|
595
|
+
padding: 0.375rem 0.75rem;
|
|
596
|
+
background: var(--bg-tertiary);
|
|
597
|
+
border: 1px solid var(--border-color);
|
|
598
|
+
border-radius: 8px;
|
|
599
|
+
font-size: 0.7rem;
|
|
600
|
+
font-weight: 500;
|
|
601
|
+
color: var(--text-secondary);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.session-badge strong {
|
|
605
|
+
color: var(--accent-purple);
|
|
606
|
+
font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.query-monitoring-controls {
|
|
610
|
+
display: flex;
|
|
611
|
+
align-items: center;
|
|
612
|
+
gap: 1rem;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.query-monitoring-count {
|
|
616
|
+
font-size: 0.8rem;
|
|
617
|
+
color: var(--text-secondary);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.query-monitoring-count strong {
|
|
621
|
+
color: var(--accent-blue);
|
|
622
|
+
font-weight: 600;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* Query Feed */
|
|
626
|
+
.query-feed {
|
|
627
|
+
max-height: 600px;
|
|
628
|
+
overflow-y: auto;
|
|
629
|
+
background: var(--bg-tertiary);
|
|
630
|
+
border: 1px solid var(--border-color);
|
|
631
|
+
border-radius: 12px;
|
|
632
|
+
padding: 1rem;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.query-feed::-webkit-scrollbar {
|
|
636
|
+
width: 8px;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.query-feed::-webkit-scrollbar-track {
|
|
640
|
+
background: var(--bg-secondary);
|
|
641
|
+
border-radius: 4px;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.query-feed::-webkit-scrollbar-thumb {
|
|
645
|
+
background: var(--border-color);
|
|
646
|
+
border-radius: 4px;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.query-feed::-webkit-scrollbar-thumb:hover {
|
|
650
|
+
background: var(--text-muted);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.query-feed-empty {
|
|
654
|
+
text-align: center;
|
|
655
|
+
padding: 3rem;
|
|
656
|
+
color: var(--text-muted);
|
|
657
|
+
font-size: 0.9rem;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.query-item {
|
|
661
|
+
background: var(--bg-card);
|
|
662
|
+
border: 1px solid var(--border-color);
|
|
663
|
+
border-radius: 8px;
|
|
664
|
+
padding: 0.875rem;
|
|
665
|
+
margin-bottom: 0.75rem;
|
|
666
|
+
font-size: 0.85rem;
|
|
667
|
+
transition: all 0.15s;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.query-item:hover {
|
|
671
|
+
border-color: var(--accent-purple);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.query-item:last-child {
|
|
675
|
+
margin-bottom: 0;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.query-header {
|
|
679
|
+
display: flex;
|
|
680
|
+
justify-content: space-between;
|
|
681
|
+
align-items: center;
|
|
682
|
+
margin-bottom: 0.5rem;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.query-meta {
|
|
686
|
+
display: flex;
|
|
687
|
+
gap: 1rem;
|
|
688
|
+
font-size: 0.75rem;
|
|
689
|
+
color: var(--text-muted);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.query-timestamp {
|
|
693
|
+
color: var(--text-secondary);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.query-duration {
|
|
697
|
+
color: var(--accent-amber);
|
|
698
|
+
font-weight: 600;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.query-duration.fast {
|
|
702
|
+
color: var(--accent-green);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.query-duration.slow {
|
|
706
|
+
color: var(--accent-rose);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.query-name {
|
|
710
|
+
color: var(--text-secondary);
|
|
711
|
+
font-style: italic;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.query-expand-btn {
|
|
715
|
+
background: transparent;
|
|
716
|
+
border: none;
|
|
717
|
+
color: var(--text-muted);
|
|
718
|
+
cursor: pointer;
|
|
719
|
+
padding: 0.25rem 0.5rem;
|
|
720
|
+
font-size: 0.7rem;
|
|
721
|
+
transition: all 0.15s;
|
|
722
|
+
border-radius: 4px;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.query-expand-btn:hover {
|
|
726
|
+
background: var(--bg-tertiary);
|
|
727
|
+
color: var(--accent-purple);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.query-sql-wrapper {
|
|
731
|
+
margin-bottom: 0.5rem;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.query-sql {
|
|
735
|
+
padding: 0.75rem;
|
|
736
|
+
background: var(--bg-primary);
|
|
737
|
+
border: 1px solid var(--border-color);
|
|
738
|
+
border-radius: 6px;
|
|
739
|
+
font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
|
|
740
|
+
font-size: 0.75rem;
|
|
741
|
+
color: var(--accent-blue);
|
|
742
|
+
word-wrap: break-word;
|
|
743
|
+
overflow-x: auto;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.query-sql-collapsed {
|
|
747
|
+
white-space: nowrap;
|
|
748
|
+
overflow: hidden;
|
|
749
|
+
text-overflow: ellipsis;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.query-sql-expanded {
|
|
753
|
+
white-space: pre-wrap;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.query-source {
|
|
757
|
+
font-size: 0.7rem;
|
|
758
|
+
color: var(--text-muted);
|
|
759
|
+
font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.query-source a {
|
|
763
|
+
color: var(--accent-purple);
|
|
764
|
+
text-decoration: none;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.query-source a:hover {
|
|
768
|
+
text-decoration: underline;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/* Download Dropdown */
|
|
772
|
+
.download-dropdown {
|
|
773
|
+
position: relative;
|
|
774
|
+
display: inline-block;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.download-menu {
|
|
778
|
+
position: absolute;
|
|
779
|
+
top: calc(100% + 0.5rem);
|
|
780
|
+
right: 0;
|
|
781
|
+
min-width: 180px;
|
|
782
|
+
background: var(--bg-card);
|
|
783
|
+
border: 1px solid var(--border-color);
|
|
784
|
+
border-radius: 10px;
|
|
785
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
786
|
+
z-index: 100;
|
|
787
|
+
overflow: hidden;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.download-menu a {
|
|
791
|
+
display: block;
|
|
792
|
+
padding: 0.75rem 1rem;
|
|
793
|
+
color: var(--text-secondary);
|
|
794
|
+
text-decoration: none;
|
|
795
|
+
font-size: 0.875rem;
|
|
796
|
+
transition: all 0.15s ease;
|
|
797
|
+
border-bottom: 1px solid var(--border-color);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.download-menu a:last-child {
|
|
801
|
+
border-bottom: none;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.download-menu a:hover {
|
|
805
|
+
background: var(--bg-tertiary);
|
|
806
|
+
color: var(--text-primary);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.btn-small {
|
|
810
|
+
height: 32px;
|
|
811
|
+
padding: 0 1rem;
|
|
812
|
+
font-size: 0.8rem;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.btn-danger {
|
|
816
|
+
background: var(--accent-rose);
|
|
817
|
+
color: white;
|
|
818
|
+
border-color: var(--accent-rose);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.btn-danger:hover {
|
|
822
|
+
opacity: 0.9;
|
|
823
|
+
transform: translateY(-1px);
|
|
824
|
+
}
|
|
543
825
|
</style>
|
|
544
826
|
</head>
|
|
545
827
|
<body>
|
|
@@ -999,7 +999,9 @@
|
|
|
999
999
|
}
|
|
1000
1000
|
|
|
1001
1001
|
// Render saved records on page load
|
|
1002
|
-
document.addEventListener('DOMContentLoaded',
|
|
1002
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1003
|
+
renderSavedRecords();
|
|
1004
|
+
});
|
|
1003
1005
|
|
|
1004
1006
|
// ==========================================
|
|
1005
1007
|
// EXPLAIN ANALYZE FUNCTIONALITY
|
|
@@ -1090,6 +1092,186 @@
|
|
|
1090
1092
|
return params;
|
|
1091
1093
|
}
|
|
1092
1094
|
|
|
1095
|
+
// Render EXPLAIN ANALYZE with colored nodes and problem detection
|
|
1096
|
+
function renderExplainAnalysis(data) {
|
|
1097
|
+
let html = '';
|
|
1098
|
+
|
|
1099
|
+
// Show summary if there are problems
|
|
1100
|
+
if (data.summary && data.summary.total_problems > 0) {
|
|
1101
|
+
html += '<div class="explain-summary explain-summary-' + data.summary.status + '">';
|
|
1102
|
+
html += '<div class="explain-summary-header">';
|
|
1103
|
+
html += '<span class="explain-summary-icon">' + data.summary.status_icon + '</span>';
|
|
1104
|
+
html += '<span class="explain-summary-title">' + escapeHtml(data.summary.status_text) + '</span>';
|
|
1105
|
+
html += '</div>';
|
|
1106
|
+
html += '<div class="explain-summary-stats">';
|
|
1107
|
+
if (data.summary.critical_problems > 0) {
|
|
1108
|
+
html += '<span class="explain-summary-stat critical">🔴 ' + data.summary.critical_problems + ' critical</span>';
|
|
1109
|
+
}
|
|
1110
|
+
if (data.summary.warnings > 0) {
|
|
1111
|
+
html += '<span class="explain-summary-stat warning">⚠️ ' + data.summary.warnings + ' warnings</span>';
|
|
1112
|
+
}
|
|
1113
|
+
html += '</div>';
|
|
1114
|
+
html += '</div>';
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Show stats
|
|
1118
|
+
if (data.stats) {
|
|
1119
|
+
html += '<div class="explain-stats">';
|
|
1120
|
+
if (data.stats.planning_time !== undefined) {
|
|
1121
|
+
const planningClass = data.stats.planning_time > 100 ? 'stat-warning' : '';
|
|
1122
|
+
html += '<div class="explain-stat ' + planningClass + '">';
|
|
1123
|
+
html += '<span class="explain-stat-label">Planning Time</span>';
|
|
1124
|
+
html += '<span class="explain-stat-value">' + data.stats.planning_time.toFixed(2) + ' ms</span>';
|
|
1125
|
+
html += '</div>';
|
|
1126
|
+
}
|
|
1127
|
+
if (data.stats.execution_time !== undefined) {
|
|
1128
|
+
const execClass = data.stats.execution_time > 1000 ? 'stat-critical' : (data.stats.execution_time > 100 ? 'stat-warning' : '');
|
|
1129
|
+
html += '<div class="explain-stat ' + execClass + '">';
|
|
1130
|
+
html += '<span class="explain-stat-label">Execution Time</span>';
|
|
1131
|
+
html += '<span class="explain-stat-value">' + data.stats.execution_time.toFixed(2) + ' ms</span>';
|
|
1132
|
+
html += '</div>';
|
|
1133
|
+
}
|
|
1134
|
+
if (data.stats.total_cost !== undefined) {
|
|
1135
|
+
html += '<div class="explain-stat">';
|
|
1136
|
+
html += '<span class="explain-stat-label">Total Cost</span>';
|
|
1137
|
+
html += '<span class="explain-stat-value">' + data.stats.total_cost.toFixed(2) + '</span>';
|
|
1138
|
+
html += '</div>';
|
|
1139
|
+
}
|
|
1140
|
+
if (data.stats.rows_estimated !== undefined) {
|
|
1141
|
+
html += '<div class="explain-stat">';
|
|
1142
|
+
html += '<span class="explain-stat-label">Estimated Rows</span>';
|
|
1143
|
+
html += '<span class="explain-stat-value">' + data.stats.rows_estimated + '</span>';
|
|
1144
|
+
html += '</div>';
|
|
1145
|
+
}
|
|
1146
|
+
html += '</div>';
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Show problems list
|
|
1150
|
+
if (data.problems && data.problems.length > 0) {
|
|
1151
|
+
html += '<div class="explain-problems">';
|
|
1152
|
+
html += '<div class="explain-problems-header">⚠️ Detected Issues</div>';
|
|
1153
|
+
html += '<div class="explain-problems-list">';
|
|
1154
|
+
|
|
1155
|
+
data.problems.forEach(problem => {
|
|
1156
|
+
const severityClass = 'problem-' + problem.severity;
|
|
1157
|
+
const severityIcon = problem.severity === 'critical' ? '🔴' : (problem.severity === 'warning' ? '⚠️' : 'ℹ️');
|
|
1158
|
+
|
|
1159
|
+
html += '<div class="explain-problem ' + severityClass + '">';
|
|
1160
|
+
html += '<div class="explain-problem-header">';
|
|
1161
|
+
html += '<span class="explain-problem-icon">' + severityIcon + '</span>';
|
|
1162
|
+
html += '<span class="explain-problem-message">' + escapeHtml(problem.message) + '</span>';
|
|
1163
|
+
if (problem.line_number) {
|
|
1164
|
+
html += '<span class="explain-problem-line">Line ' + problem.line_number + '</span>';
|
|
1165
|
+
}
|
|
1166
|
+
html += '</div>';
|
|
1167
|
+
if (problem.details) {
|
|
1168
|
+
html += '<div class="explain-problem-details">' + escapeHtml(problem.details) + '</div>';
|
|
1169
|
+
}
|
|
1170
|
+
if (problem.recommendation) {
|
|
1171
|
+
html += '<div class="explain-problem-recommendation">💡 ' + escapeHtml(problem.recommendation) + '</div>';
|
|
1172
|
+
}
|
|
1173
|
+
html += '</div>';
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
html += '</div>';
|
|
1177
|
+
html += '</div>';
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Show annotated EXPLAIN output
|
|
1181
|
+
if (data.annotated_lines && data.annotated_lines.length > 0) {
|
|
1182
|
+
html += '<div class="explain-output">';
|
|
1183
|
+
html += '<div class="explain-output-header">';
|
|
1184
|
+
html += '📊 Execution Plan';
|
|
1185
|
+
html += '<button class="btn-copy-small" onclick="copyExplainOutput()" title="Copy to clipboard">📋 Copy</button>';
|
|
1186
|
+
html += '</div>';
|
|
1187
|
+
html += '<div class="explain-lines">';
|
|
1188
|
+
|
|
1189
|
+
data.annotated_lines.forEach(line => {
|
|
1190
|
+
let lineClass = 'explain-line';
|
|
1191
|
+
let nodeColorClass = '';
|
|
1192
|
+
let tooltip = '';
|
|
1193
|
+
|
|
1194
|
+
// Add color class based on node type
|
|
1195
|
+
if (line.node_info) {
|
|
1196
|
+
nodeColorClass = 'node-' + line.node_info.color;
|
|
1197
|
+
tooltip = line.node_info.description;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Highlight timing lines
|
|
1201
|
+
if (line.is_timing) {
|
|
1202
|
+
lineClass += ' explain-line-timing';
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Check if this line has a problem
|
|
1206
|
+
const lineProblem = data.problems?.find(p => p.line_number === line.line_number);
|
|
1207
|
+
if (lineProblem) {
|
|
1208
|
+
lineClass += ' explain-line-problem';
|
|
1209
|
+
lineClass += ' problem-' + lineProblem.severity;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
html += '<div class="' + lineClass + '" data-line="' + line.line_number + '">';
|
|
1213
|
+
|
|
1214
|
+
// Line number
|
|
1215
|
+
html += '<span class="explain-line-number">' + line.line_number + '</span>';
|
|
1216
|
+
|
|
1217
|
+
// Content with indentation
|
|
1218
|
+
const indentPx = line.indent_level * 20;
|
|
1219
|
+
html += '<span class="explain-line-content ' + nodeColorClass + '" style="padding-left: ' + indentPx + 'px"';
|
|
1220
|
+
if (tooltip) {
|
|
1221
|
+
html += ' title="' + escapeHtml(tooltip) + '"';
|
|
1222
|
+
}
|
|
1223
|
+
html += '>';
|
|
1224
|
+
|
|
1225
|
+
// Highlight node type if present
|
|
1226
|
+
let lineText = escapeHtml(line.text.trim());
|
|
1227
|
+
if (line.node_type) {
|
|
1228
|
+
lineText = lineText.replace(
|
|
1229
|
+
escapeHtml(line.node_type),
|
|
1230
|
+
'<span class="explain-node-type">' + escapeHtml(line.node_type) + '</span>'
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Highlight metrics
|
|
1235
|
+
lineText = lineText
|
|
1236
|
+
.replace(/\bcost=([\d.]+)\.\.([\d.]+)/g, '<span class="explain-metric">cost=<span class="metric-value">$1..$2</span></span>')
|
|
1237
|
+
.replace(/\brows=(\d+)/g, '<span class="explain-metric">rows=<span class="metric-value">$1</span></span>')
|
|
1238
|
+
.replace(/\bactual time=([\d.]+)\.\.([\d.]+)/g, '<span class="explain-metric">actual time=<span class="metric-value">$1..$2</span></span>')
|
|
1239
|
+
.replace(/\bloops=(\d+)/g, '<span class="explain-metric">loops=<span class="metric-value">$1</span></span>');
|
|
1240
|
+
|
|
1241
|
+
html += lineText;
|
|
1242
|
+
html += '</span>';
|
|
1243
|
+
|
|
1244
|
+
// Problem indicator
|
|
1245
|
+
if (lineProblem) {
|
|
1246
|
+
const problemIcon = lineProblem.severity === 'critical' ? '🔴' : '⚠️';
|
|
1247
|
+
html += '<span class="explain-line-problem-indicator" title="' + escapeHtml(lineProblem.message) + '">' + problemIcon + '</span>';
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
html += '</div>';
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
html += '</div>';
|
|
1254
|
+
html += '</div>';
|
|
1255
|
+
} else {
|
|
1256
|
+
// Fallback to raw output
|
|
1257
|
+
html += '<div class="explain-result">' + escapeHtml(data.explain) + '</div>';
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
return html;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Copy EXPLAIN output to clipboard
|
|
1264
|
+
function copyExplainOutput() {
|
|
1265
|
+
const lines = document.querySelectorAll('.explain-line-content');
|
|
1266
|
+
const text = Array.from(lines).map(line => line.textContent).join('\n');
|
|
1267
|
+
|
|
1268
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
1269
|
+
showToast('EXPLAIN output copied to clipboard');
|
|
1270
|
+
}).catch(() => {
|
|
1271
|
+
showToast('Failed to copy', 'error');
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1093
1275
|
// Execute EXPLAIN ANALYZE with parameters
|
|
1094
1276
|
async function executeExplainAnalyze() {
|
|
1095
1277
|
const loading = document.getElementById('explain-loading');
|
|
@@ -1113,28 +1295,7 @@
|
|
|
1113
1295
|
loading.style.display = 'none';
|
|
1114
1296
|
|
|
1115
1297
|
if (data.success) {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
// Show stats if available
|
|
1119
|
-
if (data.stats) {
|
|
1120
|
-
html += '<div class="explain-stats">';
|
|
1121
|
-
if (data.stats.planning_time) {
|
|
1122
|
-
html += `<div class="explain-stat"><span class="explain-stat-label">Planning Time</span><span class="explain-stat-value">${data.stats.planning_time} ms</span></div>`;
|
|
1123
|
-
}
|
|
1124
|
-
if (data.stats.execution_time) {
|
|
1125
|
-
html += `<div class="explain-stat"><span class="explain-stat-label">Execution Time</span><span class="explain-stat-value">${data.stats.execution_time} ms</span></div>`;
|
|
1126
|
-
}
|
|
1127
|
-
if (data.stats.total_cost) {
|
|
1128
|
-
html += `<div class="explain-stat"><span class="explain-stat-label">Total Cost</span><span class="explain-stat-value">${data.stats.total_cost}</span></div>`;
|
|
1129
|
-
}
|
|
1130
|
-
if (data.stats.rows) {
|
|
1131
|
-
html += `<div class="explain-stat"><span class="explain-stat-label">Rows</span><span class="explain-stat-value">${data.stats.rows}</span></div>`;
|
|
1132
|
-
}
|
|
1133
|
-
html += '</div>';
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
html += `<div class="explain-result">${escapeHtml(data.explain)}</div>`;
|
|
1137
|
-
content.innerHTML = html;
|
|
1298
|
+
content.innerHTML = renderExplainAnalysis(data);
|
|
1138
1299
|
} else {
|
|
1139
1300
|
content.innerHTML = `<div class="error-message">${escapeHtml(data.error || 'Failed to run EXPLAIN ANALYZE')}</div>`;
|
|
1140
1301
|
}
|