pg_reports 0.7.0 → 0.8.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 +50 -0
- data/README.md +36 -150
- data/app/controllers/pg_reports/dashboard_controller.rb +143 -3
- data/app/views/layouts/pg_reports/application.html.erb +213 -6
- data/app/views/pg_reports/dashboard/_database_selector.html.erb +39 -0
- data/app/views/pg_reports/dashboard/_target_selector.html.erb +27 -0
- data/app/views/pg_reports/dashboard/index.html.erb +143 -50
- data/app/views/pg_reports/dashboard/show.html.erb +4 -0
- data/bin/pg_reports +85 -0
- data/config/locales/en.yml +18 -7
- data/config/locales/ru.yml +18 -7
- data/config/locales/uk.yml +18 -7
- data/config/routes.rb +3 -0
- data/lib/pg_reports/configuration.rb +32 -2
- data/lib/pg_reports/connection/error_translator.rb +109 -0
- data/lib/pg_reports/connection/registry.rb +150 -0
- data/lib/pg_reports/connection/target.rb +111 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +22 -8
- data/lib/pg_reports/executor.rb +20 -6
- data/lib/pg_reports/modules/system.rb +32 -5
- data/lib/pg_reports/query_monitor.rb +10 -7
- data/lib/pg_reports/standalone.rb +152 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +57 -0
- metadata +11 -13
|
@@ -113,13 +113,32 @@
|
|
|
113
113
|
display: flex;
|
|
114
114
|
align-items: center;
|
|
115
115
|
gap: 0.4rem;
|
|
116
|
-
|
|
116
|
+
height: 2.125rem;
|
|
117
|
+
padding: 0 0.75rem;
|
|
117
118
|
background: var(--bg-tertiary);
|
|
118
119
|
border: 1px solid var(--border-color);
|
|
119
120
|
border-radius: 4px;
|
|
120
121
|
font-size: 0.75rem;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
.header-badge-clickable {
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
font-family: inherit;
|
|
127
|
+
color: inherit;
|
|
128
|
+
transition: border-color 0.15s ease, background 0.15s ease;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.header-badge-clickable:hover {
|
|
132
|
+
background: var(--bg-card);
|
|
133
|
+
border-color: var(--text-muted);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.badge-info-icon {
|
|
137
|
+
color: var(--text-muted);
|
|
138
|
+
font-size: 0.85rem;
|
|
139
|
+
line-height: 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
.badge-dot {
|
|
124
143
|
width: 8px;
|
|
125
144
|
height: 8px;
|
|
@@ -306,13 +325,14 @@
|
|
|
306
325
|
}
|
|
307
326
|
|
|
308
327
|
.btn-primary {
|
|
309
|
-
background:
|
|
310
|
-
color:
|
|
311
|
-
border-color:
|
|
328
|
+
background: rgba(124, 138, 246, 0.14);
|
|
329
|
+
color: var(--accent-purple);
|
|
330
|
+
border-color: rgba(124, 138, 246, 0.35);
|
|
312
331
|
}
|
|
313
332
|
|
|
314
333
|
.btn-primary:hover {
|
|
315
|
-
background:
|
|
334
|
+
background: rgba(124, 138, 246, 0.24);
|
|
335
|
+
border-color: rgba(124, 138, 246, 0.55);
|
|
316
336
|
}
|
|
317
337
|
|
|
318
338
|
.btn-secondary {
|
|
@@ -633,6 +653,51 @@
|
|
|
633
653
|
font-size: 0.9rem;
|
|
634
654
|
}
|
|
635
655
|
|
|
656
|
+
.query-monitoring-scope {
|
|
657
|
+
position: relative;
|
|
658
|
+
font-weight: 400;
|
|
659
|
+
font-size: 0.75rem;
|
|
660
|
+
color: var(--text-muted);
|
|
661
|
+
background: var(--bg-tertiary);
|
|
662
|
+
padding: 0.125rem 0.5rem;
|
|
663
|
+
border-radius: 999px;
|
|
664
|
+
cursor: help;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.query-monitoring-scope::after {
|
|
668
|
+
content: attr(data-tooltip);
|
|
669
|
+
position: absolute;
|
|
670
|
+
top: calc(100% + 8px);
|
|
671
|
+
left: 0;
|
|
672
|
+
z-index: 100;
|
|
673
|
+
width: max-content;
|
|
674
|
+
max-width: 320px;
|
|
675
|
+
padding: 0.6rem 0.75rem;
|
|
676
|
+
background: var(--bg-card);
|
|
677
|
+
color: var(--text-secondary);
|
|
678
|
+
border: 1px solid var(--border-color);
|
|
679
|
+
border-radius: 6px;
|
|
680
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
|
|
681
|
+
font-size: 0.75rem;
|
|
682
|
+
font-weight: 400;
|
|
683
|
+
line-height: 1.5;
|
|
684
|
+
white-space: normal;
|
|
685
|
+
text-align: left;
|
|
686
|
+
opacity: 0;
|
|
687
|
+
visibility: hidden;
|
|
688
|
+
transform: translateY(-4px);
|
|
689
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
690
|
+
pointer-events: none;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.query-monitoring-scope:hover::after,
|
|
694
|
+
.query-monitoring-scope:focus::after,
|
|
695
|
+
.query-monitoring-scope:focus-visible::after {
|
|
696
|
+
opacity: 1;
|
|
697
|
+
visibility: visible;
|
|
698
|
+
transform: translateY(0);
|
|
699
|
+
}
|
|
700
|
+
|
|
636
701
|
.monitoring-indicator {
|
|
637
702
|
width: 7px;
|
|
638
703
|
height: 7px;
|
|
@@ -989,6 +1054,125 @@
|
|
|
989
1054
|
.modal-small {
|
|
990
1055
|
max-width: 360px;
|
|
991
1056
|
}
|
|
1057
|
+
|
|
1058
|
+
.modal-medium {
|
|
1059
|
+
max-width: 560px;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* Database selector — used inside .live-monitoring-title and .breadcrumb.
|
|
1063
|
+
Sizes to fit its longest option (no artificial cap), capped at the
|
|
1064
|
+
parent's available width so it never overruns neighboring controls. */
|
|
1065
|
+
.db-selector-form {
|
|
1066
|
+
display: inline-flex;
|
|
1067
|
+
align-items: center;
|
|
1068
|
+
gap: 0.5rem;
|
|
1069
|
+
margin: 0;
|
|
1070
|
+
max-width: 100%;
|
|
1071
|
+
min-width: 0;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.db-selector-label {
|
|
1075
|
+
font-size: 0.75rem;
|
|
1076
|
+
color: var(--text-secondary);
|
|
1077
|
+
text-transform: uppercase;
|
|
1078
|
+
letter-spacing: 0.5px;
|
|
1079
|
+
font-weight: 600;
|
|
1080
|
+
white-space: nowrap;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
.db-selector {
|
|
1084
|
+
background: var(--bg-card, rgba(255, 255, 255, 0.04));
|
|
1085
|
+
color: var(--text-primary);
|
|
1086
|
+
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
|
|
1087
|
+
border-radius: 6px;
|
|
1088
|
+
padding: 0.375rem 0.625rem;
|
|
1089
|
+
font-size: 0.875rem;
|
|
1090
|
+
cursor: pointer;
|
|
1091
|
+
width: auto;
|
|
1092
|
+
max-width: 100%;
|
|
1093
|
+
min-width: 0;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
.db-selector option.is-default {
|
|
1097
|
+
font-weight: 700;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.db-selector:hover {
|
|
1101
|
+
border-color: var(--accent-blue, #6b9fe8);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
.db-selector:focus {
|
|
1105
|
+
outline: none;
|
|
1106
|
+
border-color: var(--accent-blue, #6b9fe8);
|
|
1107
|
+
box-shadow: 0 0 0 2px rgba(107, 159, 232, 0.2);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.db-selector-static {
|
|
1111
|
+
display: inline-flex;
|
|
1112
|
+
align-items: center;
|
|
1113
|
+
gap: 0.5rem;
|
|
1114
|
+
font-size: 0.875rem;
|
|
1115
|
+
color: var(--text-secondary);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
.db-selector-static strong {
|
|
1119
|
+
color: var(--text-primary);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.db-selector-error {
|
|
1123
|
+
display: inline-flex;
|
|
1124
|
+
align-items: center;
|
|
1125
|
+
gap: 0.375rem;
|
|
1126
|
+
padding: 0.375rem 0.625rem;
|
|
1127
|
+
background: rgba(232, 107, 107, 0.1);
|
|
1128
|
+
border: 1px solid rgba(232, 107, 107, 0.3);
|
|
1129
|
+
border-radius: 6px;
|
|
1130
|
+
font-size: 0.8125rem;
|
|
1131
|
+
color: var(--accent-red, #e86b6b);
|
|
1132
|
+
cursor: help;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.breadcrumb-spacer {
|
|
1136
|
+
flex: 1;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.breadcrumb {
|
|
1140
|
+
display: flex;
|
|
1141
|
+
align-items: center;
|
|
1142
|
+
gap: 0.5rem;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.db-error-banner {
|
|
1146
|
+
background: rgba(232, 107, 107, 0.08);
|
|
1147
|
+
border: 1px solid rgba(232, 107, 107, 0.3);
|
|
1148
|
+
border-radius: 8px;
|
|
1149
|
+
padding: 0.875rem 1.125rem;
|
|
1150
|
+
margin-bottom: 1rem;
|
|
1151
|
+
display: flex;
|
|
1152
|
+
flex-direction: column;
|
|
1153
|
+
gap: 0.375rem;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
.db-error-banner strong {
|
|
1157
|
+
color: var(--accent-red, #e86b6b);
|
|
1158
|
+
font-size: 0.9375rem;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.db-error-banner span {
|
|
1162
|
+
font-size: 0.875rem;
|
|
1163
|
+
color: var(--text-secondary);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.db-error-banner code {
|
|
1167
|
+
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
1168
|
+
font-size: 0.8125rem;
|
|
1169
|
+
background: rgba(0, 0, 0, 0.25);
|
|
1170
|
+
padding: 0.375rem 0.625rem;
|
|
1171
|
+
border-radius: 4px;
|
|
1172
|
+
color: var(--text-primary);
|
|
1173
|
+
display: inline-block;
|
|
1174
|
+
width: fit-content;
|
|
1175
|
+
}
|
|
992
1176
|
</style>
|
|
993
1177
|
</head>
|
|
994
1178
|
<body>
|
|
@@ -1003,7 +1187,10 @@
|
|
|
1003
1187
|
<div id="ide-menu" class="ide-menu"></div>
|
|
1004
1188
|
|
|
1005
1189
|
<script>
|
|
1006
|
-
|
|
1190
|
+
// Trailing slash stripped so `${pgReportsRoot}/foo` never becomes `//foo` —
|
|
1191
|
+
// a protocol-relative URL (host "foo") — when the engine is mounted at "/"
|
|
1192
|
+
// (e.g. standalone mode), where the mount path resolves to "/".
|
|
1193
|
+
const pgReportsRoot = (document.querySelector('meta[name="pg-reports-root"]')?.content || '/pg_reports').replace(/\/+$/, '');
|
|
1007
1194
|
window.PG_REPORTS_I18N = <%= raw I18n.t("pg_reports.ui").to_json %>;
|
|
1008
1195
|
|
|
1009
1196
|
// Format strings with %{var} placeholders
|
|
@@ -1012,6 +1199,26 @@
|
|
|
1012
1199
|
return template.replace(/%\{(\w+)\}/g, function(_, k) { return vars[k] != null ? vars[k] : ''; });
|
|
1013
1200
|
};
|
|
1014
1201
|
|
|
1202
|
+
// Close the topmost open modal on ESC. Applies to every `.modal` that has a
|
|
1203
|
+
// close (×) button — it triggers that button so each modal's own close
|
|
1204
|
+
// handler (and any cleanup) runs.
|
|
1205
|
+
document.addEventListener('keydown', function(e) {
|
|
1206
|
+
if (e.key !== 'Escape' && e.key !== 'Esc') return;
|
|
1207
|
+
const open = Array.prototype.slice.call(document.querySelectorAll('.modal'))
|
|
1208
|
+
.filter(function(m) {
|
|
1209
|
+
return getComputedStyle(m).display !== 'none' && m.querySelector('.modal-close');
|
|
1210
|
+
});
|
|
1211
|
+
if (!open.length) return;
|
|
1212
|
+
e.preventDefault();
|
|
1213
|
+
const modal = open[open.length - 1];
|
|
1214
|
+
const closeBtn = modal.querySelector('.modal-close');
|
|
1215
|
+
if (closeBtn) {
|
|
1216
|
+
closeBtn.click();
|
|
1217
|
+
} else {
|
|
1218
|
+
modal.style.display = 'none';
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1015
1222
|
function showToast(message, type = 'success') {
|
|
1016
1223
|
const toast = document.getElementById('toast');
|
|
1017
1224
|
toast.textContent = message;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<%# locals: (show_label: true) %>
|
|
2
|
+
<% if @available_databases.present? && @available_databases.size > 1 %>
|
|
3
|
+
<form class="db-selector-form"
|
|
4
|
+
action="<%= switch_database_path %>"
|
|
5
|
+
method="post"
|
|
6
|
+
data-turbo="false">
|
|
7
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
|
8
|
+
<% if local_assigns.fetch(:show_label, true) %>
|
|
9
|
+
<label class="db-selector-label" for="pg-reports-db-select">
|
|
10
|
+
<%= t("pg_reports.ui.database_selector.label", default: "Database") %>
|
|
11
|
+
</label>
|
|
12
|
+
<% end %>
|
|
13
|
+
<select id="pg-reports-db-select"
|
|
14
|
+
name="database"
|
|
15
|
+
class="db-selector"
|
|
16
|
+
onchange="this.form.submit()">
|
|
17
|
+
<% @available_databases.each do |row| %>
|
|
18
|
+
<option value="<%= row["name"] %>"
|
|
19
|
+
class="<%= "is-default" if row["name"] == @target_default_database %>"
|
|
20
|
+
<%= "selected" if row["name"] == @selected_database %>>
|
|
21
|
+
<%= row["name"] %><%= " (#{row["size"]})" if row["size"].present? %>
|
|
22
|
+
</option>
|
|
23
|
+
<% end %>
|
|
24
|
+
</select>
|
|
25
|
+
</form>
|
|
26
|
+
<% elsif @available_databases.present? && @available_databases.size == 1 %>
|
|
27
|
+
<span class="db-selector-static">
|
|
28
|
+
<% if local_assigns.fetch(:show_label, true) %>
|
|
29
|
+
<span class="db-selector-label">
|
|
30
|
+
<%= t("pg_reports.ui.database_selector.label", default: "Database") %>:
|
|
31
|
+
</span>
|
|
32
|
+
<% end %>
|
|
33
|
+
<strong><%= @available_databases.first["name"] %></strong>
|
|
34
|
+
</span>
|
|
35
|
+
<% elsif @database_error %>
|
|
36
|
+
<span class="db-selector-error" title="<%= @database_error[:detail] %>">
|
|
37
|
+
⚠ <%= @database_error[:title] %>
|
|
38
|
+
</span>
|
|
39
|
+
<% end %>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%# locals: (show_label: true) %>
|
|
2
|
+
<% if @available_targets.present? && @available_targets.size > 1 %>
|
|
3
|
+
<form class="db-selector-form"
|
|
4
|
+
action="<%= switch_target_path %>"
|
|
5
|
+
method="post"
|
|
6
|
+
data-turbo="false">
|
|
7
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
|
8
|
+
<% if local_assigns.fetch(:show_label, true) %>
|
|
9
|
+
<label class="db-selector-label" for="pg-reports-target-select">
|
|
10
|
+
<%= t("pg_reports.ui.target_selector.label", default: "Target") %>
|
|
11
|
+
</label>
|
|
12
|
+
<% end %>
|
|
13
|
+
<select id="pg-reports-target-select"
|
|
14
|
+
name="target"
|
|
15
|
+
class="db-selector"
|
|
16
|
+
onchange="this.form.submit()">
|
|
17
|
+
<% default_target_name = PgReports.connection_registry.default_name %>
|
|
18
|
+
<% @available_targets.each do |target| %>
|
|
19
|
+
<option value="<%= target.name %>"
|
|
20
|
+
class="<%= "is-default" if target.name == default_target_name %>"
|
|
21
|
+
<%= "selected" if target.name == @selected_target %>>
|
|
22
|
+
<%= target.name %><%= " (#{target.default_database})" if target.default_database.present? %>
|
|
23
|
+
</option>
|
|
24
|
+
<% end %>
|
|
25
|
+
</select>
|
|
26
|
+
</form>
|
|
27
|
+
<% end %>
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
<%= csrf_meta_tags %>
|
|
2
2
|
|
|
3
|
+
<% if @database_error %>
|
|
4
|
+
<div class="db-error-banner">
|
|
5
|
+
<strong><%= @database_error[:title] %></strong>
|
|
6
|
+
<span><%= @database_error[:detail] %></span>
|
|
7
|
+
<% if @database_error[:hint] %>
|
|
8
|
+
<code><%= @database_error[:hint] %></code>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
3
13
|
<header class="header">
|
|
4
14
|
<div class="logo">
|
|
5
15
|
<div class="logo-icon">🐘</div>
|
|
@@ -17,28 +27,34 @@
|
|
|
17
27
|
<button class="btn btn-small btn-muted" onclick="showResetConfirmModal()" id="reset-btn">
|
|
18
28
|
<%= t("pg_reports.ui.actions.reset_statistics") %>
|
|
19
29
|
</button>
|
|
20
|
-
<% else %>
|
|
21
|
-
<button class="btn btn-small btn-primary" onclick="enablePgStatStatements(this)" id="enable-btn">
|
|
22
|
-
<%= t("pg_reports.ui.actions.create_extension") %>
|
|
23
|
-
</button>
|
|
24
|
-
<button class="btn-info" onclick="showPgStatInfo()">?</button>
|
|
25
30
|
<% end %>
|
|
26
31
|
<button class="btn-info" onclick="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>">⚙️</button>
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
|
|
33
|
+
<% if !@pg_stat_status[:connected] %>
|
|
34
|
+
<div class="header-badge" id="pg-stat-badge">
|
|
35
|
+
<span class="badge-dot error"></span>
|
|
36
|
+
<span><%= t("pg_reports.ui.status.disconnected") %></span>
|
|
37
|
+
</div>
|
|
38
|
+
<% elsif @pg_stat_status[:ready] %>
|
|
39
|
+
<div class="header-badge" id="pg-stat-badge">
|
|
29
40
|
<span class="badge-dot"></span>
|
|
30
41
|
<span><%= t("pg_reports.ui.status.pg_stat_ready") %></span>
|
|
31
|
-
|
|
42
|
+
</div>
|
|
43
|
+
<% elsif @pg_stat_status[:extension_installed] %>
|
|
44
|
+
<button type="button" class="header-badge header-badge-clickable" id="pg-stat-badge"
|
|
45
|
+
onclick="showPgStatInfo()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
|
|
32
46
|
<span class="badge-dot warning"></span>
|
|
33
|
-
<span><%= t("pg_reports.ui.status.
|
|
34
|
-
|
|
47
|
+
<span><%= t("pg_reports.ui.status.not_preloaded") %></span>
|
|
48
|
+
<span class="badge-info-icon" aria-hidden="true">ⓘ</span>
|
|
49
|
+
</button>
|
|
50
|
+
<% else %>
|
|
51
|
+
<button type="button" class="header-badge header-badge-clickable" id="pg-stat-badge"
|
|
52
|
+
onclick="showCreateExtensionModal()" title="<%= t("pg_reports.ui.status.click_for_details") %>">
|
|
35
53
|
<span class="badge-dot warning"></span>
|
|
36
|
-
<span><%= t("pg_reports.ui.status.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<% end %>
|
|
41
|
-
</div>
|
|
54
|
+
<span><%= t("pg_reports.ui.status.extension_missing") %></span>
|
|
55
|
+
<span class="badge-info-icon" aria-hidden="true">ⓘ</span>
|
|
56
|
+
</button>
|
|
57
|
+
<% end %>
|
|
42
58
|
</div>
|
|
43
59
|
</header>
|
|
44
60
|
|
|
@@ -71,6 +87,25 @@ pg_stat_statements.track = all</pre>
|
|
|
71
87
|
</div>
|
|
72
88
|
</div>
|
|
73
89
|
|
|
90
|
+
<!-- Create extension modal -->
|
|
91
|
+
<div id="create-extension-modal" class="modal" style="display: none;">
|
|
92
|
+
<div class="modal-content modal-medium">
|
|
93
|
+
<div class="modal-header">
|
|
94
|
+
<h3><%= t("pg_reports.ui.modals.create_extension_title") %></h3>
|
|
95
|
+
<button class="modal-close" onclick="closeCreateExtensionModal()">×</button>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="modal-body">
|
|
98
|
+
<p><%= t("pg_reports.ui.modals.create_extension_intro") %></p>
|
|
99
|
+
<pre>CREATE EXTENSION IF NOT EXISTS pg_stat_statements;</pre>
|
|
100
|
+
<p class="warning-subtext"><%= t("pg_reports.ui.modals.create_extension_note") %></p>
|
|
101
|
+
<div class="modal-actions">
|
|
102
|
+
<button class="btn btn-secondary" onclick="closeCreateExtensionModal()"><%= t("pg_reports.ui.actions.cancel") %></button>
|
|
103
|
+
<button class="btn btn-primary" onclick="enablePgStatStatements(this)" id="modal-create-extension-btn"><%= t("pg_reports.ui.actions.create_extension") %></button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
74
109
|
<!-- Reset statistics confirmation modal -->
|
|
75
110
|
<div id="reset-confirm-modal" class="modal" style="display: none;">
|
|
76
111
|
<div class="modal-content modal-small">
|
|
@@ -138,9 +173,8 @@ pg_stat_statements.track = all</pre>
|
|
|
138
173
|
<div class="live-monitoring-title">
|
|
139
174
|
<span class="live-indicator"></span>
|
|
140
175
|
<span><%= t("pg_reports.ui.monitoring.live_title") %></span>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</span>
|
|
176
|
+
<%= render "target_selector", show_label: false %>
|
|
177
|
+
<%= render "database_selector", show_label: false %>
|
|
144
178
|
</div>
|
|
145
179
|
<div class="live-monitoring-controls">
|
|
146
180
|
<span class="live-monitoring-interval"><%= t("pg_reports.ui.monitoring.update_interval") %></span>
|
|
@@ -258,6 +292,9 @@ pg_stat_statements.track = all</pre>
|
|
|
258
292
|
<div class="query-monitoring-title">
|
|
259
293
|
<span class="monitoring-indicator" id="monitor-indicator"></span>
|
|
260
294
|
<span><%= t("pg_reports.ui.monitoring.query_monitor_title") %></span>
|
|
295
|
+
<span class="query-monitoring-scope" tabindex="0" data-tooltip="<%= t("pg_reports.ui.monitoring.scope_note_long", default: "Query Monitor subscribes to ActiveSupport::Notifications in the host application's process. It captures every SQL the host app runs, on whichever database the host app is connected to. The dashboard's database selector does not change this scope.") %>">
|
|
296
|
+
<%= t("pg_reports.ui.monitoring.scope_note_short", default: "scope: host application") %>
|
|
297
|
+
</span>
|
|
261
298
|
<span class="session-badge" id="session-badge" style="display: none;">
|
|
262
299
|
<%= t("pg_reports.ui.monitoring.session_label") %> <strong id="session-id"></strong>
|
|
263
300
|
</span>
|
|
@@ -297,11 +334,20 @@ pg_stat_statements.track = all</pre>
|
|
|
297
334
|
|
|
298
335
|
<div class="categories-grid">
|
|
299
336
|
<% @categories.each do |category_key, category| %>
|
|
300
|
-
|
|
301
|
-
|
|
337
|
+
<%
|
|
338
|
+
requires_pg_stat = (category_key == :queries && !@pg_stat_status[:ready])
|
|
339
|
+
target_disabled_reason = category_disabled_reason(category_key)
|
|
340
|
+
is_disabled = requires_pg_stat || target_disabled_reason.present?
|
|
341
|
+
%>
|
|
342
|
+
<div class="category-card<%= ' disabled' if is_disabled %>">
|
|
343
|
+
<% if requires_pg_stat %>
|
|
302
344
|
<div class="category-warning">
|
|
303
345
|
<%= t("pg_reports.ui.categories.requires_pg_stat") %>
|
|
304
346
|
</div>
|
|
347
|
+
<% elsif target_disabled_reason %>
|
|
348
|
+
<div class="category-warning">
|
|
349
|
+
<%= target_disabled_reason %>
|
|
350
|
+
</div>
|
|
305
351
|
<% end %>
|
|
306
352
|
<div class="category-header">
|
|
307
353
|
<div class="category-icon" style="background: <%= category[:color] %>20; color: <%= category[:color] %>;">
|
|
@@ -313,7 +359,7 @@ pg_stat_statements.track = all</pre>
|
|
|
313
359
|
|
|
314
360
|
<div class="reports-list">
|
|
315
361
|
<% category[:reports].each do |report_key, report| %>
|
|
316
|
-
<% if
|
|
362
|
+
<% if is_disabled %>
|
|
317
363
|
<div class="report-link disabled">
|
|
318
364
|
<div class="report-link-info">
|
|
319
365
|
<span class="report-link-name">
|
|
@@ -342,7 +388,37 @@ pg_stat_statements.track = all</pre>
|
|
|
342
388
|
<% end %>
|
|
343
389
|
</div>
|
|
344
390
|
|
|
391
|
+
<footer class="dashboard-footer">
|
|
392
|
+
<a href="https://github.com/deadalice/pg_reports" target="_blank" rel="noopener noreferrer">github.com/deadalice/pg_reports</a>
|
|
393
|
+
<span class="footer-sep">·</span>
|
|
394
|
+
<a href="mailto:deadalice@gmail.com">deadalice@gmail.com</a>
|
|
395
|
+
</footer>
|
|
396
|
+
|
|
345
397
|
<style>
|
|
398
|
+
.dashboard-footer {
|
|
399
|
+
margin-top: 2rem;
|
|
400
|
+
padding-top: 1.25rem;
|
|
401
|
+
border-top: 1px solid var(--border-color);
|
|
402
|
+
display: flex;
|
|
403
|
+
align-items: center;
|
|
404
|
+
justify-content: center;
|
|
405
|
+
gap: 0.6rem;
|
|
406
|
+
flex-wrap: wrap;
|
|
407
|
+
color: var(--text-muted);
|
|
408
|
+
font-size: 0.8rem;
|
|
409
|
+
}
|
|
410
|
+
.dashboard-footer a {
|
|
411
|
+
color: var(--text-secondary);
|
|
412
|
+
text-decoration: none;
|
|
413
|
+
}
|
|
414
|
+
.dashboard-footer a:hover {
|
|
415
|
+
color: var(--accent-purple);
|
|
416
|
+
text-decoration: underline;
|
|
417
|
+
}
|
|
418
|
+
.dashboard-footer .footer-sep {
|
|
419
|
+
opacity: 0.5;
|
|
420
|
+
}
|
|
421
|
+
|
|
346
422
|
.header-actions {
|
|
347
423
|
display: flex;
|
|
348
424
|
align-items: center;
|
|
@@ -354,15 +430,26 @@ pg_stat_statements.track = all</pre>
|
|
|
354
430
|
font-size: 0.8rem;
|
|
355
431
|
}
|
|
356
432
|
|
|
433
|
+
/* Keep the header action button the same height as the status badge and
|
|
434
|
+
the settings button (all 2.125rem); inline-flex centers the label so no
|
|
435
|
+
vertical padding is needed. */
|
|
436
|
+
.header-actions .btn {
|
|
437
|
+
height: 2.125rem;
|
|
438
|
+
padding: 0 1rem;
|
|
439
|
+
}
|
|
440
|
+
|
|
357
441
|
.btn-info {
|
|
358
|
-
width:
|
|
359
|
-
height:
|
|
442
|
+
width: 2.125rem;
|
|
443
|
+
height: 2.125rem;
|
|
360
444
|
padding: 0;
|
|
445
|
+
display: inline-flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
justify-content: center;
|
|
361
448
|
background: var(--bg-tertiary);
|
|
362
449
|
color: var(--text-muted);
|
|
363
450
|
border: 1px solid var(--border-color);
|
|
364
|
-
border-radius:
|
|
365
|
-
font-size: 0.
|
|
451
|
+
border-radius: 4px;
|
|
452
|
+
font-size: 0.9rem;
|
|
366
453
|
font-weight: 600;
|
|
367
454
|
cursor: pointer;
|
|
368
455
|
transition: all 0.15s;
|
|
@@ -410,7 +497,7 @@ pg_stat_statements.track = all</pre>
|
|
|
410
497
|
|
|
411
498
|
.category-warning {
|
|
412
499
|
padding: 0.625rem 1rem;
|
|
413
|
-
margin: -
|
|
500
|
+
margin: -1rem -1rem 1rem -1rem;
|
|
414
501
|
background: rgba(245, 158, 11, 0.1);
|
|
415
502
|
border-bottom: 1px solid rgba(245, 158, 11, 0.2);
|
|
416
503
|
border-radius: 6px 6px 0 0;
|
|
@@ -613,22 +700,6 @@ pg_stat_statements.track = all</pre>
|
|
|
613
700
|
font-size: 1rem;
|
|
614
701
|
}
|
|
615
702
|
|
|
616
|
-
.database-badge {
|
|
617
|
-
margin-left: 0.5rem;
|
|
618
|
-
padding: 0.375rem 0.75rem;
|
|
619
|
-
background: var(--bg-tertiary);
|
|
620
|
-
border: 1px solid var(--border-color);
|
|
621
|
-
border-radius: 8px;
|
|
622
|
-
font-size: 0.75rem;
|
|
623
|
-
font-weight: 500;
|
|
624
|
-
color: var(--text-secondary);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
.database-badge strong {
|
|
628
|
-
color: var(--text-primary);
|
|
629
|
-
font-weight: 600;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
703
|
.live-indicator {
|
|
633
704
|
width: 8px;
|
|
634
705
|
height: 8px;
|
|
@@ -852,6 +923,18 @@ pg_stat_statements.track = all</pre>
|
|
|
852
923
|
if (e.target === this) closePgStatModal();
|
|
853
924
|
});
|
|
854
925
|
|
|
926
|
+
function showCreateExtensionModal() {
|
|
927
|
+
document.getElementById('create-extension-modal').style.display = 'flex';
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function closeCreateExtensionModal() {
|
|
931
|
+
document.getElementById('create-extension-modal').style.display = 'none';
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
document.getElementById('create-extension-modal')?.addEventListener('click', function(e) {
|
|
935
|
+
if (e.target === this) closeCreateExtensionModal();
|
|
936
|
+
});
|
|
937
|
+
|
|
855
938
|
function showResetConfirmModal() {
|
|
856
939
|
document.getElementById('reset-confirm-modal').style.display = 'flex';
|
|
857
940
|
}
|
|
@@ -918,6 +1001,7 @@ pg_stat_statements.track = all</pre>
|
|
|
918
1001
|
} else {
|
|
919
1002
|
showToast(data.message, 'error');
|
|
920
1003
|
if (data.requires_restart) {
|
|
1004
|
+
closeCreateExtensionModal();
|
|
921
1005
|
showPgStatInfo();
|
|
922
1006
|
}
|
|
923
1007
|
button.disabled = false;
|
|
@@ -990,13 +1074,12 @@ pg_stat_statements.track = all</pre>
|
|
|
990
1074
|
if (!panel) return;
|
|
991
1075
|
|
|
992
1076
|
panel.style.display = 'block';
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
</div>
|
|
1077
|
+
|
|
1078
|
+
// Replace only the metrics grid; preserve the header (which holds the
|
|
1079
|
+
// database selector) so the user can still switch databases when metrics
|
|
1080
|
+
// are unavailable on the current one.
|
|
1081
|
+
const grid = panel.querySelector('.live-metrics-grid');
|
|
1082
|
+
const fallbackHTML = `
|
|
1000
1083
|
<div style="padding: 2rem; text-align: center; color: var(--text-muted);">
|
|
1001
1084
|
<p style="margin-bottom: 1rem;">${PG_REPORTS_I18N.errors.unable_fetch_metrics}</p>
|
|
1002
1085
|
<p style="font-size: 0.875rem;">${PG_REPORTS_I18N.errors.possible_causes}</p>
|
|
@@ -1010,6 +1093,16 @@ pg_stat_statements.track = all</pre>
|
|
|
1010
1093
|
</button>
|
|
1011
1094
|
</div>
|
|
1012
1095
|
`;
|
|
1096
|
+
|
|
1097
|
+
if (grid) {
|
|
1098
|
+
grid.outerHTML = fallbackHTML;
|
|
1099
|
+
} else {
|
|
1100
|
+
panel.insertAdjacentHTML('beforeend', fallbackHTML);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Surface the unavailable status next to the live indicator.
|
|
1104
|
+
const indicator = panel.querySelector('.live-indicator');
|
|
1105
|
+
if (indicator) indicator.classList.add('error');
|
|
1013
1106
|
}
|
|
1014
1107
|
|
|
1015
1108
|
function startPolling() {
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
<span><%= @categories[@category][:name] %></span>
|
|
8
8
|
<span>/</span>
|
|
9
9
|
<span><%= @report_info[:name] %></span>
|
|
10
|
+
|
|
11
|
+
<span class="breadcrumb-spacer"></span>
|
|
12
|
+
<%= render "target_selector" %>
|
|
13
|
+
<%= render "database_selector" %>
|
|
10
14
|
</nav>
|
|
11
15
|
|
|
12
16
|
<div class="report-header">
|