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
12fn format_uptime(secs: u64) -> String {
14 let d = secs / 86400;
15 let h = (secs % 86400) / 3600;
16 let m = (secs % 3600) / 60;
17 if d > 0 {
18 format!("{d}d {h}h {m}m")
19 } else if h > 0 {
20 format!("{h}h {m}m")
21 } else if m > 0 {
22 format!("{m}m")
23 } else {
24 format!("{secs}s")
25 }
26}
27const REFRESH_MS_OPTIONS: &[(Option<u32>, &str)] = &[
29 (None, "手动"),
30 (Some(500), "500ms"),
31 (Some(1000), "1s"),
32 (Some(2000), "2s"),
33 (Some(5000), "5s"),
34];
35
36#[allow(non_snake_case)]
40#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
41pub(super) fn ServerStatusTab() -> Element {
42 #[cfg(target_arch = "wasm32")]
43 use crate::api::database::system_status::get_server_status;
44 use crate::api::database::system_status::ServerStatus;
45
46 let mut status = use_signal(|| Option::<ServerStatus>::None);
47 let mut loading = use_signal(|| true);
48 let mut error = use_signal(|| Option::<String>::None);
49 let mut refresh_ms: Signal<Option<u32>> = use_signal(|| None);
51
52 let mut load_once = move || {
53 loading.set(true);
54 #[cfg(target_arch = "wasm32")]
55 {
56 spawn(async move {
57 match get_server_status().await {
58 Ok(s) => {
59 status.set(Some(s));
60 error.set(None);
61 }
62 Err(e) => error.set(Some(e.to_string())),
63 }
64 loading.set(false);
65 });
66 }
67 #[cfg(not(target_arch = "wasm32"))]
68 {
69 loading.set(false);
70 }
71 };
72
73 use_effect(move || {
74 load_once();
75 });
76
77 use_future(move || async move {
80 #[cfg(target_arch = "wasm32")]
81 {
82 loop {
83 let ms = refresh_ms().unwrap_or(0);
84 if ms == 0 {
85 crate::utils::time::sleep_ms(200).await;
86 continue;
87 }
88 crate::utils::time::sleep_ms(ms).await;
89 if refresh_ms().is_none() {
90 continue;
91 }
92 loading.set(true);
93 spawn(async move {
94 match get_server_status().await {
95 Ok(s) => {
96 status.set(Some(s));
97 error.set(None);
98 }
99 Err(e) => error.set(Some(e.to_string())),
100 }
101 loading.set(false);
102 });
103 }
104 }
105 #[cfg(not(target_arch = "wasm32"))]
106 {
107 let _ = (status, loading, error, refresh_ms);
108 }
109 });
110
111 let current = status.read().clone();
112 let cpu_pct = current
114 .as_ref()
115 .map(|s| format!("{:.1}%", s.host.cpu_usage))
116 .unwrap_or_default();
117 let load_1 = current
118 .as_ref()
119 .map(|s| format!("{:.2}", s.host.load_avg_1))
120 .unwrap_or_default();
121 let cache_rows: Vec<(String, u64, u64, u64, String)> = current
123 .as_ref()
124 .map(|s| {
125 s.caches
126 .iter()
127 .map(|c| {
128 (
129 c.name.clone(),
130 c.entry_count,
131 c.hits,
132 c.misses,
133 format!("{:.1}%", c.hit_rate * 100.0),
134 )
135 })
136 .collect()
137 })
138 .unwrap_or_default();
139
140 rsx! {
141 div { class: "space-y-6",
142 div { class: "flex items-center justify-between gap-4",
143 LoadingButton {
144 label: "刷新数据".to_string(),
145 loading: loading(),
146 variant: "sm",
147 onclick: move |_| {
148 loading.set(true);
149 #[cfg(target_arch = "wasm32")]
150 {
151 spawn(async move {
152 match get_server_status().await {
153 Ok(s) => {
154 status.set(Some(s));
155 error.set(None);
156 }
157 Err(e) => error.set(Some(e.to_string())),
158 }
159 loading.set(false);
160 });
161 }
162 #[cfg(not(target_arch = "wasm32"))]
163 {
164 loading.set(false);
165 }
166 },
167 }
168 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",
169 svg {
170 class: "w-3.5 h-3.5 text-[var(--color-paper-tertiary)]",
171 xmlns: "http://www.w3.org/2000/svg",
172 view_box: "0 0 24 24",
173 fill: "none",
174 stroke: "currentColor",
175 stroke_width: "2",
176 stroke_linecap: "round",
177 stroke_linejoin: "round",
178 circle { cx: "12", cy: "12", r: "10" }
179 polyline { points: "12 6 12 12 16 14" }
180 }
181 span { class: "text-xs font-medium text-[var(--color-paper-secondary)]", "自动刷新" }
182 FormSelect {
183 trigger_class: Some(FORM_SELECT_COMPACT_CLASS),
184 value: refresh_ms(),
185 options: REFRESH_MS_OPTIONS.to_vec(),
186 onchange: move |v| refresh_ms.set(v),
187 }
188 }
189 }
190 if let Some(err) = error.read().clone() {
191 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",
192 "加载失败:{err}"
193 }
194 } else if let Some(s) = current {
195 div { class: "grid grid-cols-2 lg:grid-cols-4 gap-4",
197 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",
198 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
199 span { class: "text-xs font-semibold uppercase tracking-wider", "服务运行时间" }
200 svg {
201 class: "w-4 h-4 text-[var(--color-paper-tertiary)]",
202 xmlns: "http://www.w3.org/2000/svg",
203 view_box: "0 0 24 24",
204 fill: "none",
205 stroke: "currentColor",
206 stroke_width: "2",
207 stroke_linecap: "round",
208 stroke_linejoin: "round",
209 circle { cx: "12", cy: "12", r: "10" }
210 polyline { points: "12 6 12 12 16 14" }
211 }
212 }
213 p { class: "text-2xl font-extrabold font-mono text-[var(--color-paper-primary)] tracking-tight",
214 "{format_uptime(s.uptime_secs)}"
215 }
216 }
217 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",
218 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
219 span { class: "text-xs font-semibold uppercase tracking-wider", "DB 连接池" }
220 svg {
221 class: "w-4 h-4 text-[var(--color-paper-tertiary)]",
222 xmlns: "http://www.w3.org/2000/svg",
223 view_box: "0 0 24 24",
224 fill: "none",
225 stroke: "currentColor",
226 stroke_width: "2",
227 stroke_linecap: "round",
228 stroke_linejoin: "round",
229 path { d: "M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z" }
230 }
231 }
232 p { class: "text-2xl font-extrabold font-mono text-[var(--color-paper-primary)] tracking-tight",
233 "{s.pool_size} / {s.pool_max_size}"
234 }
235 p { class: "text-xs text-[var(--color-paper-tertiary)] mt-0.5",
236 "空闲 {s.pool_available} · 等待 {s.pool_waiting}"
237 }
238 }
239 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",
240 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
241 span { class: "text-xs font-semibold uppercase tracking-wider", "活跃会话" }
242 svg {
243 class: "w-4 h-4 text-[var(--color-paper-tertiary)]",
244 xmlns: "http://www.w3.org/2000/svg",
245 view_box: "0 0 24 24",
246 fill: "none",
247 stroke: "currentColor",
248 stroke_width: "2",
249 stroke_linecap: "round",
250 stroke_linejoin: "round",
251 path { d: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" }
252 circle { cx: "9", cy: "7", r: "4" }
253 }
254 }
255 p { class: "text-2xl font-extrabold font-mono text-[var(--color-paper-primary)] tracking-tight",
256 "{s.active_sessions}"
257 }
258 }
259 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",
260 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
261 span { class: "text-xs font-semibold uppercase tracking-wider", "应用 CPU" }
262 svg {
263 class: "w-4 h-4 text-[var(--color-paper-tertiary)]",
264 xmlns: "http://www.w3.org/2000/svg",
265 view_box: "0 0 24 24",
266 fill: "none",
267 stroke: "currentColor",
268 stroke_width: "2",
269 stroke_linecap: "round",
270 stroke_linejoin: "round",
271 rect { x: "4", y: "4", width: "16", height: "16", rx: "2" }
272 rect { x: "9", y: "9", width: "6", height: "6" }
273 }
274 }
275 p { class: "text-2xl font-extrabold font-mono text-[var(--color-paper-accent)] tracking-tight",
276 "{cpu_pct}"
277 }
278 }
279 }
280
281 div { class: "grid grid-cols-2 lg:grid-cols-4 gap-4",
283 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",
284 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
285 span { class: "text-xs font-semibold uppercase tracking-wider", "内存占用" }
286 }
287 p { class: "text-xl font-bold font-mono text-[var(--color-paper-primary)] tracking-tight",
288 "{format_bytes(s.host.used_memory as i64)} / {format_bytes(s.host.total_memory as i64)}"
289 }
290 }
291 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",
292 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
293 span { class: "text-xs font-semibold uppercase tracking-wider", "磁盘空间" }
294 }
295 p { class: "text-xl font-bold font-mono text-[var(--color-paper-primary)] tracking-tight",
296 "{format_bytes((s.host.disk_total - s.host.disk_available) as i64)} / {format_bytes(s.host.disk_total as i64)}"
297 }
298 }
299 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",
300 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
301 span { class: "text-xs font-semibold uppercase tracking-wider", "平均负载 (1m)" }
302 }
303 p { class: "text-xl font-bold font-mono text-[var(--color-paper-primary)] tracking-tight",
304 "{load_1}"
305 }
306 }
307 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",
308 div { class: "flex items-center justify-between text-[var(--color-paper-secondary)]",
309 span { class: "text-xs font-semibold uppercase tracking-wider", "宿主系统" }
310 }
311 p { class: "text-base font-semibold font-mono text-[var(--color-paper-primary)] truncate mt-0.5",
312 "{s.host.os_name}"
313 }
314 }
315 }
316
317 div { class: "bg-[var(--color-paper-entry)]/40 rounded-2xl shadow-xs border border-[var(--color-paper-border)]/70 overflow-hidden",
319 div { class: "px-5 py-4 border-b border-[var(--color-paper-border)]/70 flex items-center gap-2 select-none",
320 svg {
321 class: "w-4 h-4 text-[var(--color-paper-accent)]",
322 xmlns: "http://www.w3.org/2000/svg",
323 view_box: "0 0 24 24",
324 fill: "none",
325 stroke: "currentColor",
326 stroke_width: "2",
327 stroke_linecap: "round",
328 stroke_linejoin: "round",
329 path { d: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" }
330 }
331 span { class: "font-semibold text-sm text-[var(--color-paper-primary)]", "内存缓存指标 (Moka Cache)" }
332 }
333 div { class: "overflow-x-auto",
334 table { class: "w-full text-sm",
335 thead {
336 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",
337 th { class: "px-5 py-3.5", "缓存名称" }
338 th { class: "px-4 py-3.5 text-right whitespace-nowrap", "当前条目" }
339 th { class: "px-4 py-3.5 text-right whitespace-nowrap", "命中次数" }
340 th { class: "px-4 py-3.5 text-right whitespace-nowrap", "未命中数" }
341 th { class: "px-5 py-3.5 text-right whitespace-nowrap", "命中率" }
342 }
343 }
344 tbody {
345 for (name, entry_count, hits, misses, rate_pct) in cache_rows.iter() {
346 tr { class: "border-b border-[var(--color-paper-border)]/60 last:border-0 hover:bg-[var(--color-paper-accent-soft)]/20 transition-colors",
347 td { class: "px-5 py-3 font-mono font-medium text-[var(--color-paper-primary)]",
348 "{name}"
349 }
350 td { class: "px-4 py-3 text-right font-mono text-[var(--color-paper-secondary)]",
351 "{entry_count}"
352 }
353 td { class: "px-4 py-3 text-right font-mono text-[var(--color-paper-secondary)]",
354 "{hits}"
355 }
356 td { class: "px-4 py-3 text-right font-mono text-[var(--color-paper-secondary)]",
357 "{misses}"
358 }
359 td { class: "px-5 py-3 text-right font-mono text-[var(--color-paper-primary)] font-semibold",
360 "{rate_pct}"
361 }
362 }
363 }
364 }
365 }
366 }
367 }
368 } else if loading() {
369 DelayedSkeleton {
371 div { class: "space-y-4",
372 div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
374 for _ in 0..4 {
375 div { class: "rounded-2xl bg-paper-entry border border-paper-border p-4 space-y-2",
376 SkeletonBox { class: "h-3 w-16 rounded" }
377 SkeletonBox { class: "h-6 w-24 rounded" }
378 }
379 }
380 }
381 div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
383 for _ in 0..4 {
384 div { class: "rounded-2xl bg-paper-entry border border-paper-border p-4 space-y-2",
385 SkeletonBox { class: "h-3 w-16 rounded" }
386 SkeletonBox { class: "h-6 w-24 rounded" }
387 }
388 }
389 }
390 div { class: "rounded-2xl bg-paper-entry border border-paper-border overflow-hidden",
392 div { class: "px-4 py-3 border-b border-paper-border",
393 SkeletonBox { class: "h-4 w-24 rounded" }
394 }
395 for _ in 0..4 {
396 div { class: "flex justify-between px-4 py-3 border-b border-paper-border last:border-0",
397 SkeletonBox { class: "h-4 w-20 rounded" }
398 SkeletonBox { class: "h-4 w-12 rounded" }
399 }
400 }
401 }
402 }
403 }
404 } else {
405 div { class: "text-paper-secondary py-8", "暂无数据" }
406 }
407 }
408 }
409}