1use 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 crate::utils::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
33 let mut status = use_signal(|| Option::<DbStatus>::None);
35 let mut loading = use_signal(|| true);
36 let mut error = use_signal(|| Option::<String>::None);
37 let mut refresh_interval: Signal<Option<u32>> = use_signal(|| None);
39
40 let mut load_once = move || {
43 loading.set(true);
44 #[cfg(target_arch = "wasm32")]
45 {
46 spawn(async move {
47 match get_db_status().await {
48 Ok(s) => {
49 status.set(Some(s));
50 error.set(None);
51 }
52 Err(e) => error.set(Some(e.to_string())),
53 }
54 loading.set(false);
55 });
56 }
57 #[cfg(not(target_arch = "wasm32"))]
58 {
59 loading.set(false);
60 }
61 };
62
63 use_effect(move || {
65 load_once();
66 });
67
68 use_future(move || async move {
73 #[cfg(target_arch = "wasm32")]
74 {
75 loop {
76 let secs = refresh_interval().unwrap_or(0);
78 if secs == 0 {
79 crate::utils::time::sleep_ms(200).await;
82 continue;
83 }
84 crate::utils::time::sleep_ms(secs * 1000).await;
85 if refresh_interval().is_none() {
87 continue;
88 }
89 loading.set(true);
90 spawn(async move {
91 match get_db_status().await {
92 Ok(s) => {
93 status.set(Some(s));
94 error.set(None);
95 }
96 Err(e) => error.set(Some(e.to_string())),
97 }
98 loading.set(false);
99 });
100 }
101 }
102 #[cfg(not(target_arch = "wasm32"))]
103 {
104 let _ = (status, loading, error, refresh_interval);
105 }
106 });
107
108 let current = status.read().clone();
110
111 rsx! {
112 div { class: "space-y-6",
113 div { class: "flex items-center justify-between gap-4",
115 LoadingButton {
116 label: "刷新数据".to_string(),
117 loading: loading(),
118 variant: "sm",
119 onclick: move |_| {
120 loading.set(true);
121 #[cfg(target_arch = "wasm32")]
122 {
123 spawn(async move {
124 match get_db_status().await {
125 Ok(s) => {
126 status.set(Some(s));
127 error.set(None);
128 }
129 Err(e) => error.set(Some(e.to_string())),
130 }
131 loading.set(false);
132 });
133 }
134 #[cfg(not(target_arch = "wasm32"))]
135 {
136 loading.set(false);
137 }
138 },
139 }
140 div { class: "inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[var(--color-paper-entry)]/60 border border-[var(--color-paper-border)]/60 shadow-2xs",
141 svg {
142 class: "w-3.5 h-3.5 text-[var(--color-paper-tertiary)]",
143 xmlns: "http://www.w3.org/2000/svg",
144 view_box: "0 0 24 24",
145 fill: "none",
146 stroke: "currentColor",
147 stroke_width: "2",
148 stroke_linecap: "round",
149 stroke_linejoin: "round",
150 circle { cx: "12", cy: "12", r: "10" }
151 polyline { points: "12 6 12 12 16 14" }
152 }
153 span { class: "text-xs font-medium text-[var(--color-paper-secondary)]", "自动刷新" }
154 FormSelect {
155 trigger_class: Some(FORM_SELECT_COMPACT_CLASS),
156 value: refresh_interval(),
157 options: REFRESH_INTERVAL_OPTIONS.to_vec(),
158 onchange: move |v| refresh_interval.set(v),
159 }
160 }
161 }
162 if let Some(err) = error.read().clone() {
163 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",
164 "加载失败:{err}"
165 }
166 } else if let Some(s) = current {
167 div { class: "grid grid-cols-2 lg:grid-cols-4 gap-4",
169 div { class: "bg-[var(--color-paper-entry)]/40 rounded-2xl p-5 border border-[var(--color-paper-border)]/70 shadow-xs flex flex-col gap-1.5",
171 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
172 span { class: "text-xs font-semibold uppercase tracking-wider", "数据库大小" }
173 svg {
174 class: "w-4 h-4 text-[var(--color-paper-tertiary)]",
175 xmlns: "http://www.w3.org/2000/svg",
176 view_box: "0 0 24 24",
177 fill: "none",
178 stroke: "currentColor",
179 stroke_width: "2",
180 stroke_linecap: "round",
181 stroke_linejoin: "round",
182 ellipse { cx: "12", cy: "5", rx: "9", ry: "3" }
183 path { d: "M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" }
184 path { d: "M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" }
185 }
186 }
187 p { class: "text-2xl font-extrabold font-mono text-[var(--color-paper-primary)] tracking-tight",
188 "{format_bytes(s.db_size_bytes)}"
189 }
190 }
191 div { class: "bg-[var(--color-paper-entry)]/40 rounded-2xl p-5 border border-[var(--color-paper-border)]/70 shadow-xs flex flex-col gap-1.5",
193 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
194 span { class: "text-xs font-semibold uppercase tracking-wider", "活跃连接数" }
195 svg {
196 class: "w-4 h-4 text-[var(--color-paper-tertiary)]",
197 xmlns: "http://www.w3.org/2000/svg",
198 view_box: "0 0 24 24",
199 fill: "none",
200 stroke: "currentColor",
201 stroke_width: "2",
202 stroke_linecap: "round",
203 stroke_linejoin: "round",
204 path { d: "M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" }
205 }
206 }
207 p { class: "text-2xl font-extrabold font-mono text-[var(--color-paper-primary)] tracking-tight",
208 "{s.total_connections} / {s.max_connections}"
209 }
210 }
211 div { class: "bg-[var(--color-paper-entry)]/40 rounded-2xl p-5 border border-[var(--color-paper-border)]/70 shadow-xs flex flex-col gap-1.5",
213 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
214 span { class: "text-xs font-semibold uppercase tracking-wider", "数据表数量" }
215 svg {
216 class: "w-4 h-4 text-[var(--color-paper-tertiary)]",
217 xmlns: "http://www.w3.org/2000/svg",
218 view_box: "0 0 24 24",
219 fill: "none",
220 stroke: "currentColor",
221 stroke_width: "2",
222 stroke_linecap: "round",
223 stroke_linejoin: "round",
224 rect { x: "3", y: "3", width: "18", height: "18", rx: "2" }
225 line { x1: "3", y1: "9", x2: "21", y2: "9" }
226 line { x1: "9", y1: "21", x2: "9", y2: "9" }
227 }
228 }
229 p { class: "text-2xl font-extrabold font-mono text-[var(--color-paper-primary)] tracking-tight",
230 "{s.tables.len()}"
231 }
232 }
233 div { class: "bg-[var(--color-paper-entry)]/40 rounded-2xl p-5 border border-[var(--color-paper-border)]/70 shadow-xs flex flex-col gap-1.5",
235 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
236 span { class: "text-xs font-semibold uppercase tracking-wider", "当前迁移版本" }
237 svg {
238 class: "w-4 h-4 text-[var(--color-paper-tertiary)]",
239 xmlns: "http://www.w3.org/2000/svg",
240 view_box: "0 0 24 24",
241 fill: "none",
242 stroke: "currentColor",
243 stroke_width: "2",
244 stroke_linecap: "round",
245 stroke_linejoin: "round",
246 circle { cx: "12", cy: "12", r: "4" }
247 line { x1: "1.05", y1: "12", x2: "7", y2: "12" }
248 line { x1: "17.01", y1: "12", x2: "22.96", y2: "12" }
249 }
250 }
251 p { class: "text-2xl font-extrabold font-mono text-[var(--color-paper-accent)] tracking-tight truncate",
252 {s.migration_version.clone().map(|v| format!("v{v}")).unwrap_or_else(|| "—".to_string())}
253 }
254 }
255 }
256
257 div { class: "bg-[var(--color-paper-entry)]/40 rounded-2xl shadow-xs border border-[var(--color-paper-border)]/70 overflow-hidden",
259 div { class: "px-5 py-4 border-b border-[var(--color-paper-border)]/70 flex flex-col sm:flex-row sm:items-center justify-between gap-1 select-none",
260 div { class: "flex items-center gap-2 font-semibold text-sm text-[var(--color-paper-primary)]",
261 svg {
262 class: "w-4 h-4 text-[var(--color-paper-accent)]",
263 xmlns: "http://www.w3.org/2000/svg",
264 view_box: "0 0 24 24",
265 fill: "none",
266 stroke: "currentColor",
267 stroke_width: "2",
268 stroke_linecap: "round",
269 stroke_linejoin: "round",
270 path { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }
271 polyline { points: "14 2 14 8 20 8" }
272 }
273 "数据表空间与记录清单"
274 }
275 span { class: "text-xs text-[var(--color-paper-tertiary)]", "小表为真实行数,大表标 ~ 为统计估算" }
276 }
277 div { class: "overflow-x-auto",
278 table { class: "w-full text-sm",
279 thead {
280 tr { class: "bg-[var(--color-paper-entry)]/80 border-b border-[var(--color-paper-border)]/70 text-left text-xs font-semibold uppercase tracking-wider text-[var(--color-paper-secondary)] select-none",
281 th { class: "px-5 py-3.5", "表名" }
282 th { class: "px-4 py-3.5 text-right whitespace-nowrap", "行数" }
283 th { class: "px-4 py-3.5 text-right whitespace-nowrap", "数据大小" }
284 th { class: "px-4 py-3.5 text-right whitespace-nowrap", "索引大小" }
285 th { class: "px-4 py-3.5 text-right whitespace-nowrap", "总占用" }
286 th { class: "px-5 py-3.5 text-right whitespace-nowrap", "死元组" }
287 }
288 }
289 tbody {
290 for t in s.tables.iter() {
291 tr { class: "border-b border-[var(--color-paper-border)]/60 last:border-0 hover:bg-[var(--color-paper-accent-soft)]/20 transition-colors",
292 td { class: "px-5 py-3 font-mono font-medium text-[var(--color-paper-primary)]",
293 "{t.name}"
294 }
295 td { class: "px-4 py-3 text-right font-mono text-[var(--color-paper-secondary)]",
296 if t.row_count_estimated {
297 "~{t.row_count}"
298 } else {
299 "{t.row_count}"
300 }
301 }
302 td { class: "px-4 py-3 text-right font-mono text-[var(--color-paper-secondary)]",
303 "{format_bytes(t.table_size_bytes)}"
304 }
305 td { class: "px-4 py-3 text-right font-mono text-[var(--color-paper-secondary)]",
306 "{format_bytes(t.index_size_bytes)}"
307 }
308 td { class: "px-4 py-3 text-right font-mono text-[var(--color-paper-primary)] font-semibold",
309 "{format_bytes(t.total_size_bytes)}"
310 }
311 td { class: "px-5 py-3 text-right font-mono text-[var(--color-paper-tertiary)]",
312 "{t.dead_tuples}"
313 }
314 }
315 }
316 }
317 }
318 }
319 }
320
321 if !s.top_indexes.is_empty() {
323 div { class: "bg-[var(--color-paper-entry)]/40 rounded-2xl shadow-xs border border-[var(--color-paper-border)]/70 overflow-hidden",
324 div { class: "px-5 py-4 border-b border-[var(--color-paper-border)]/70 flex items-center gap-2 select-none",
325 svg {
326 class: "w-4 h-4 text-[var(--color-paper-accent)]",
327 xmlns: "http://www.w3.org/2000/svg",
328 view_box: "0 0 24 24",
329 fill: "none",
330 stroke: "currentColor",
331 stroke_width: "2",
332 stroke_linecap: "round",
333 stroke_linejoin: "round",
334 line { x1: "18", y1: "20", x2: "18", y2: "10" }
335 line { x1: "12", y1: "20", x2: "12", y2: "4" }
336 line { x1: "6", y1: "20", x2: "6", y2: "14" }
337 }
338 span { class: "font-semibold text-sm text-[var(--color-paper-primary)]", "索引占用 Top 10" }
339 }
340 div { class: "overflow-x-auto",
341 table { class: "w-full text-sm",
342 thead {
343 tr { class: "bg-[var(--color-paper-entry)]/80 border-b border-[var(--color-paper-border)]/70 text-left text-xs font-semibold uppercase tracking-wider text-[var(--color-paper-secondary)] select-none",
344 th { class: "px-5 py-3.5", "索引名" }
345 th { class: "px-4 py-3.5", "所属表" }
346 th { class: "px-5 py-3.5 text-right whitespace-nowrap", "占用空间" }
347 }
348 }
349 tbody {
350 for i in s.top_indexes.iter() {
351 tr { class: "border-b border-[var(--color-paper-border)]/60 last:border-0 hover:bg-[var(--color-paper-accent-soft)]/20 transition-colors",
352 td { class: "px-5 py-3 font-mono text-xs font-medium text-[var(--color-paper-primary)]",
353 "{i.name}"
354 }
355 td { class: "px-4 py-3 font-mono text-xs text-[var(--color-paper-secondary)]",
356 "{i.table_name}"
357 }
358 td { class: "px-5 py-3 text-right font-mono text-xs text-[var(--color-paper-primary)] font-semibold",
359 "{format_bytes(i.size_bytes)}"
360 }
361 }
362 }
363 }
364 }
365 }
366 }
367 }
368
369 div { class: "bg-[var(--color-paper-entry)]/40 rounded-2xl shadow-xs border border-[var(--color-paper-border)]/70 overflow-hidden",
371 div { class: "px-5 py-4 border-b border-[var(--color-paper-border)]/70 flex items-center gap-2 select-none",
372 svg {
373 class: "w-4 h-4 text-[var(--color-paper-accent)]",
374 xmlns: "http://www.w3.org/2000/svg",
375 view_box: "0 0 24 24",
376 fill: "none",
377 stroke: "currentColor",
378 stroke_width: "2",
379 stroke_linecap: "round",
380 stroke_linejoin: "round",
381 path { d: "M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" }
382 }
383 span { class: "font-semibold text-sm text-[var(--color-paper-primary)]",
384 "当前活跃连接 ({s.active_connections.len()})"
385 }
386 }
387 div { class: "overflow-x-auto",
388 table { class: "w-full text-sm",
389 thead {
390 tr { class: "bg-[var(--color-paper-entry)]/80 border-b border-[var(--color-paper-border)]/70 text-left text-xs font-semibold uppercase tracking-wider text-[var(--color-paper-secondary)] select-none",
391 th { class: "px-5 py-3.5 w-20", "PID" }
392 th { class: "px-4 py-3.5 w-28", "用户" }
393 th { class: "px-4 py-3.5 w-24", "状态" }
394 th { class: "px-4 py-3.5 w-24 text-right whitespace-nowrap", "执行时长" }
395 th { class: "px-5 py-3.5", "SQL 查询语句" }
396 }
397 }
398 tbody {
399 for c in s.active_connections.iter() {
400 tr { class: "border-b border-[var(--color-paper-border)]/60 last:border-0 hover:bg-[var(--color-paper-accent-soft)]/20 transition-colors",
401 td { class: "px-5 py-3 font-mono text-xs text-[var(--color-paper-secondary)]",
402 "{c.pid}"
403 }
404 td { class: "px-4 py-3 font-mono text-xs text-[var(--color-paper-secondary)]",
405 "{c.user}"
406 }
407 td { class: "px-4 py-3 text-xs text-[var(--color-paper-secondary)]",
408 {c.state.clone().unwrap_or_else(|| "—".to_string())}
409 }
410 td { class: "px-4 py-3 text-right font-mono text-xs text-[var(--color-paper-secondary)]",
411 {
412 c.query_duration_secs
413 .map(|d| format!("{:.1}s", d))
414 .unwrap_or_else(|| "—".to_string())
415 }
416 }
417 td { class: "px-5 py-3 font-mono text-xs text-[var(--color-paper-secondary)] max-w-lg truncate",
418 {c.query.clone().unwrap_or_else(|| "—".to_string())}
419 }
420 }
421 }
422 }
423 }
424 }
425 }
426 } else if loading() {
427 DelayedSkeleton {
429 div { class: "space-y-4",
430 div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
432 for _ in 0..4 {
433 div { class: "rounded-2xl bg-paper-entry border border-paper-border p-4 space-y-2",
434 SkeletonBox { class: "h-3 w-16 rounded" }
435 SkeletonBox { class: "h-6 w-24 rounded" }
436 }
437 }
438 }
439 div { class: "rounded-2xl bg-paper-entry border border-paper-border overflow-hidden",
441 div { class: "px-4 py-3 border-b border-paper-border",
442 SkeletonBox { class: "h-4 w-40 rounded" }
443 }
444 for _ in 0..5 {
445 div { class: "flex justify-between px-4 py-3 border-b border-paper-border last:border-0",
446 SkeletonBox { class: "h-4 w-24 rounded" }
447 SkeletonBox { class: "h-4 w-16 rounded" }
448 }
449 }
450 }
451 }
452 }
453 } else {
454 div { class: "text-paper-secondary py-8", "暂无数据" }
455 }
456 }
457 }
458}