yggdrasil/pages/admin/system/
db_status.rs1use dioxus::prelude::*;
4
5use crate::components::forms::{FormSelect, FORM_SELECT_COMPACT_CLASS};
6use crate::components::skeletons::atoms::SkeletonBox;
7use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
8use crate::components::ui::LoadingButton;
9
10use super::format_bytes;
11
12const REFRESH_INTERVAL_OPTIONS: &[(Option<u32>, &str)] = &[
14 (None, "手动"),
15 (Some(1), "1s"),
16 (Some(2), "2s"),
17 (Some(5), "5s"),
18 (Some(30), "30s"),
19];
20
21#[allow(non_snake_case)]
24#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
27pub(super) fn DbStatusTab() -> Element {
28 use crate::api::database::status::DbStatus;
29 #[cfg(target_arch = "wasm32")]
31 use crate::api::database::status::get_db_status;
32 use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS};
33
34 let mut status = use_signal(|| Option::<DbStatus>::None);
36 let mut loading = use_signal(|| true);
37 let mut error = use_signal(|| Option::<String>::None);
38 let mut refresh_interval: Signal<Option<u32>> = use_signal(|| None);
40
41 let mut load_once = move || {
44 loading.set(true);
45 #[cfg(target_arch = "wasm32")]
46 {
47 spawn(async move {
48 match get_db_status().await {
49 Ok(s) => {
50 status.set(Some(s));
51 error.set(None);
52 }
53 Err(e) => error.set(Some(e.to_string())),
54 }
55 loading.set(false);
56 });
57 }
58 #[cfg(not(target_arch = "wasm32"))]
59 {
60 loading.set(false);
61 }
62 };
63
64 use_effect(move || {
66 load_once();
67 });
68
69 use_future(move || async move {
74 #[cfg(target_arch = "wasm32")]
75 {
76 loop {
77 let secs = refresh_interval().unwrap_or(0);
79 if secs == 0 {
80 crate::utils::time::sleep_ms(200).await;
83 continue;
84 }
85 crate::utils::time::sleep_ms(secs * 1000).await;
86 if refresh_interval().is_none() {
88 continue;
89 }
90 loading.set(true);
91 spawn(async move {
92 match get_db_status().await {
93 Ok(s) => {
94 status.set(Some(s));
95 error.set(None);
96 }
97 Err(e) => error.set(Some(e.to_string())),
98 }
99 loading.set(false);
100 });
101 }
102 }
103 #[cfg(not(target_arch = "wasm32"))]
104 {
105 let _ = (status, loading, error, refresh_interval);
106 }
107 });
108
109 let current = status.read().clone();
111
112 rsx! {
113 div { class: "space-y-6",
114 div { class: "flex items-center justify-between",
116 LoadingButton {
117 label: "刷新".to_string(),
118 loading: loading(),
119 variant: "sm",
120 onclick: move |_| {
121 loading.set(true);
122 #[cfg(target_arch = "wasm32")]
123 {
124 spawn(async move {
125 match get_db_status().await {
126 Ok(s) => {
127 status.set(Some(s));
128 error.set(None);
129 }
130 Err(e) => error.set(Some(e.to_string())),
131 }
132 loading.set(false);
133 });
134 }
135 #[cfg(not(target_arch = "wasm32"))]
136 {
137 loading.set(false);
138 }
139 },
140 }
141 div { class: "flex items-center gap-2",
142 span { class: "text-sm text-paper-secondary", "自动刷新" }
143 FormSelect {
144 trigger_class: Some(FORM_SELECT_COMPACT_CLASS),
145 value: refresh_interval(),
146 options: REFRESH_INTERVAL_OPTIONS.to_vec(),
147 onchange: move |v| refresh_interval.set(v),
148 }
149 }
150 }
151
152 if let Some(err) = error.read().clone() {
153 div { class: "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 text-sm text-red-700 dark:text-red-300",
154 "加载失败:{err}"
155 }
156 } else if let Some(s) = current {
157 div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
159 div { class: "{ADMIN_CARD_CLASS} p-4",
160 p { class: "text-xs text-paper-secondary", "数据库总大小" }
161 p { class: "mt-1 text-lg font-semibold text-paper-primary",
162 "{format_bytes(s.db_size_bytes)}"
163 }
164 }
165 div { class: "{ADMIN_CARD_CLASS} p-4",
166 p { class: "text-xs text-paper-secondary", "连接数" }
167 p { class: "mt-1 text-lg font-semibold text-paper-primary",
168 "{s.total_connections} / {s.max_connections}"
169 }
170 }
171 div { class: "{ADMIN_CARD_CLASS} p-4",
172 p { class: "text-xs text-paper-secondary", "表数量" }
173 p { class: "mt-1 text-lg font-semibold text-paper-primary",
174 "{s.tables.len()}"
175 }
176 }
177 div { class: "{ADMIN_CARD_CLASS} p-4",
178 p { class: "text-xs text-paper-secondary", "迁移版本" }
179 p { class: "mt-1 text-lg font-semibold text-paper-primary truncate",
180 {s.migration_version.clone().unwrap_or_else(|| "—".to_string())}
181 }
182 }
183 }
184
185 div { class: "{ADMIN_TABLE_CLASS}",
187 div { class: "px-4 py-3 border-b border-paper-border text-sm font-medium text-paper-primary",
188 "表清单(行数:小表为真实值,大表标 ~ 为估算)"
189 }
190 div { class: "overflow-x-auto",
191 table { class: "w-full text-sm",
192 thead {
193 tr { class: "border-b border-paper-border text-left text-paper-secondary",
194 th { class: "px-4 py-2 font-medium", "表名" }
195 th { class: "px-4 py-2 font-medium text-right",
196 "行数"
197 }
198 th { class: "px-4 py-2 font-medium text-right",
199 "表大小"
200 }
201 th { class: "px-4 py-2 font-medium text-right",
202 "索引大小"
203 }
204 th { class: "px-4 py-2 font-medium text-right",
205 "总大小"
206 }
207 th { class: "px-4 py-2 font-medium text-right",
208 "死元组"
209 }
210 }
211 }
212 tbody {
213 for t in s.tables.iter() {
214 tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
215 td { class: "px-4 py-2 font-mono text-paper-primary",
216 "{t.name}"
217 }
218 td { class: "px-4 py-2 text-right text-paper-secondary",
219 if t.row_count_estimated {
220 "~{t.row_count}"
221 } else {
222 "{t.row_count}"
223 }
224 }
225 td { class: "px-4 py-2 text-right text-paper-secondary",
226 "{format_bytes(t.table_size_bytes)}"
227 }
228 td { class: "px-4 py-2 text-right text-paper-secondary",
229 "{format_bytes(t.index_size_bytes)}"
230 }
231 td { class: "px-4 py-2 text-right text-paper-primary font-medium",
232 "{format_bytes(t.total_size_bytes)}"
233 }
234 td { class: "px-4 py-2 text-right text-paper-secondary",
235 "{t.dead_tuples}"
236 }
237 }
238 }
239 }
240 }
241 }
242 }
243
244 if !s.top_indexes.is_empty() {
246 div { class: "{ADMIN_TABLE_CLASS}",
247 div { class: "px-4 py-3 border-b border-paper-border text-sm font-medium text-paper-primary",
248 "索引占用 Top 10"
249 }
250 div { class: "overflow-x-auto",
251 table { class: "w-full text-sm",
252 thead {
253 tr { class: "border-b border-paper-border text-left text-paper-secondary",
254 th { class: "px-4 py-2 font-medium", "索引名" }
255 th { class: "px-4 py-2 font-medium", "所属表" }
256 th { class: "px-4 py-2 font-medium text-right",
257 "大小"
258 }
259 }
260 }
261 tbody {
262 for i in s.top_indexes.iter() {
263 tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
264 td { class: "px-4 py-2 font-mono text-paper-primary",
265 "{i.name}"
266 }
267 td { class: "px-4 py-2 font-mono text-paper-secondary",
268 "{i.table_name}"
269 }
270 td { class: "px-4 py-2 text-right text-paper-secondary",
271 "{format_bytes(i.size_bytes)}"
272 }
273 }
274 }
275 }
276 }
277 }
278 }
279 }
280
281 div { class: "{ADMIN_TABLE_CLASS}",
283 div { class: "px-4 py-3 border-b border-paper-border text-sm font-medium text-paper-primary",
284 "活跃连接({s.active_connections.len()})"
285 }
286 div { class: "overflow-x-auto",
287 table { class: "w-full text-sm",
288 thead {
289 tr { class: "border-b border-paper-border text-left text-paper-secondary",
290 th { class: "px-4 py-2 font-medium", "PID" }
291 th { class: "px-4 py-2 font-medium", "用户" }
292 th { class: "px-4 py-2 font-medium", "状态" }
293 th { class: "px-4 py-2 font-medium text-right",
294 "时长(秒)"
295 }
296 th { class: "px-4 py-2 font-medium", "查询" }
297 }
298 }
299 tbody {
300 for c in s.active_connections.iter() {
301 tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
302 td { class: "px-4 py-2 text-paper-secondary",
303 "{c.pid}"
304 }
305 td { class: "px-4 py-2 text-paper-secondary",
306 "{c.user}"
307 }
308 td { class: "px-4 py-2 text-paper-secondary",
309 {c.state.clone().unwrap_or_else(|| "—".to_string())}
310 }
311 td { class: "px-4 py-2 text-right text-paper-secondary",
312 {
313 c.query_duration_secs
314 .map(|d| format!("{:.1}", d))
315 .unwrap_or_else(|| "—".to_string())
316 }
317 }
318 td { class: "px-4 py-2 font-mono text-xs text-paper-secondary max-w-md truncate",
319 {c.query.clone().unwrap_or_else(|| "—".to_string())}
320 }
321 }
322 }
323 }
324 }
325 }
326 }
327 } else if loading() {
328 DelayedSkeleton {
330 div { class: "space-y-4",
331 div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
333 for _ in 0..4 {
334 div { class: "rounded-2xl bg-paper-entry border border-paper-border p-4 space-y-2",
335 SkeletonBox { class: "h-3 w-16 rounded" }
336 SkeletonBox { class: "h-6 w-24 rounded" }
337 }
338 }
339 }
340 div { class: "rounded-2xl bg-paper-entry border border-paper-border overflow-hidden",
342 div { class: "px-4 py-3 border-b border-paper-border",
343 SkeletonBox { class: "h-4 w-40 rounded" }
344 }
345 for _ in 0..5 {
346 div { class: "flex justify-between px-4 py-3 border-b border-paper-border last:border-0",
347 SkeletonBox { class: "h-4 w-24 rounded" }
348 SkeletonBox { class: "h-4 w-16 rounded" }
349 }
350 }
351 }
352 }
353 }
354 } else {
355 div { class: "text-paper-secondary py-8", "暂无数据" }
356 }
357 }
358 }
359}