yggdrasil/pages/admin/system/
server_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
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 use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS};
46
47 let mut status = use_signal(|| Option::<ServerStatus>::None);
48 let mut loading = use_signal(|| true);
49 let mut error = use_signal(|| Option::<String>::None);
50 let mut refresh_ms: Signal<Option<u32>> = use_signal(|| None);
52
53 let mut load_once = move || {
54 loading.set(true);
55 #[cfg(target_arch = "wasm32")]
56 {
57 spawn(async move {
58 match get_server_status().await {
59 Ok(s) => {
60 status.set(Some(s));
61 error.set(None);
62 }
63 Err(e) => error.set(Some(e.to_string())),
64 }
65 loading.set(false);
66 });
67 }
68 #[cfg(not(target_arch = "wasm32"))]
69 {
70 loading.set(false);
71 }
72 };
73
74 use_effect(move || {
75 load_once();
76 });
77
78 use_future(move || async move {
81 #[cfg(target_arch = "wasm32")]
82 {
83 loop {
84 let ms = refresh_ms().unwrap_or(0);
85 if ms == 0 {
86 crate::utils::time::sleep_ms(200).await;
87 continue;
88 }
89 crate::utils::time::sleep_ms(ms).await;
90 if refresh_ms().is_none() {
91 continue;
92 }
93 loading.set(true);
94 spawn(async move {
95 match get_server_status().await {
96 Ok(s) => {
97 status.set(Some(s));
98 error.set(None);
99 }
100 Err(e) => error.set(Some(e.to_string())),
101 }
102 loading.set(false);
103 });
104 }
105 }
106 #[cfg(not(target_arch = "wasm32"))]
107 {
108 let _ = (status, loading, error, refresh_ms);
109 }
110 });
111
112 let current = status.read().clone();
113 let cpu_pct = current
115 .as_ref()
116 .map(|s| format!("{:.1}%", s.host.cpu_usage))
117 .unwrap_or_default();
118 let load_1 = current
119 .as_ref()
120 .map(|s| format!("{:.2}", s.host.load_avg_1))
121 .unwrap_or_default();
122 let cache_rows: Vec<(String, u64, u64, u64, String)> = current
124 .as_ref()
125 .map(|s| {
126 s.caches
127 .iter()
128 .map(|c| {
129 (
130 c.name.clone(),
131 c.entry_count,
132 c.hits,
133 c.misses,
134 format!("{:.1}%", c.hit_rate * 100.0),
135 )
136 })
137 .collect()
138 })
139 .unwrap_or_default();
140
141 rsx! {
142 div { class: "space-y-6",
143 div { class: "flex items-center justify-between",
144 LoadingButton {
145 label: "刷新".to_string(),
146 loading: loading(),
147 variant: "sm",
148 onclick: move |_| {
149 loading.set(true);
150 #[cfg(target_arch = "wasm32")]
151 {
152 spawn(async move {
153 match get_server_status().await {
154 Ok(s) => {
155 status.set(Some(s));
156 error.set(None);
157 }
158 Err(e) => error.set(Some(e.to_string())),
159 }
160 loading.set(false);
161 });
162 }
163 #[cfg(not(target_arch = "wasm32"))]
164 {
165 loading.set(false);
166 }
167 },
168 }
169 div { class: "flex items-center gap-2",
170 span { class: "text-sm text-paper-secondary", "自动刷新" }
171 FormSelect {
172 trigger_class: Some(FORM_SELECT_COMPACT_CLASS),
173 value: refresh_ms(),
174 options: REFRESH_MS_OPTIONS.to_vec(),
175 onchange: move |v| refresh_ms.set(v),
176 }
177 }
178 }
179
180 if let Some(err) = error.read().clone() {
181 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",
182 "加载失败:{err}"
183 }
184 } else if let Some(s) = current {
185 div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
187 div { class: "{ADMIN_CARD_CLASS} p-4",
188 p { class: "text-xs text-paper-secondary", "运行时间" }
189 p { class: "mt-1 text-lg font-semibold text-paper-primary",
190 "{format_uptime(s.uptime_secs)}"
191 }
192 }
193 div { class: "{ADMIN_CARD_CLASS} p-4",
194 p { class: "text-xs text-paper-secondary", "DB 连接池" }
195 p { class: "mt-1 text-lg font-semibold text-paper-primary",
196 "{s.pool_size} / {s.pool_max_size}"
197 }
198 p { class: "text-xs text-paper-secondary",
199 "空闲 {s.pool_available} · 等待 {s.pool_waiting}"
200 }
201 }
202 div { class: "{ADMIN_CARD_CLASS} p-4",
203 p { class: "text-xs text-paper-secondary", "活跃会话" }
204 p { class: "mt-1 text-lg font-semibold text-paper-primary",
205 "{s.active_sessions}"
206 }
207 }
208 div { class: "{ADMIN_CARD_CLASS} p-4",
209 p { class: "text-xs text-paper-secondary", "CPU" }
210 p { class: "mt-1 text-lg font-semibold text-paper-primary",
211 "{cpu_pct}"
212 }
213 }
214 }
215
216 div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
218 div { class: "{ADMIN_CARD_CLASS} p-4",
219 p { class: "text-xs text-paper-secondary", "内存" }
220 p { class: "mt-1 text-lg font-semibold text-paper-primary",
221 "{format_bytes(s.host.used_memory as i64)} / {format_bytes(s.host.total_memory as i64)}"
222 }
223 }
224 div { class: "{ADMIN_CARD_CLASS} p-4",
225 p { class: "text-xs text-paper-secondary", "磁盘" }
226 p { class: "mt-1 text-lg font-semibold text-paper-primary",
227 "{format_bytes((s.host.disk_total - s.host.disk_available) as i64)} / {format_bytes(s.host.disk_total as i64)}"
228 }
229 }
230 div { class: "{ADMIN_CARD_CLASS} p-4",
231 p { class: "text-xs text-paper-secondary", "Load (1m)" }
232 p { class: "mt-1 text-lg font-semibold text-paper-primary",
233 "{load_1}"
234 }
235 }
236 div { class: "{ADMIN_CARD_CLASS} p-4",
237 p { class: "text-xs text-paper-secondary", "系统" }
238 p { class: "mt-1 text-sm font-medium text-paper-primary truncate",
239 "{s.host.os_name}"
240 }
241 }
242 }
243
244 div { class: "{ADMIN_TABLE_CLASS}",
246 div { class: "px-4 py-3 border-b border-paper-border text-sm font-medium text-paper-primary",
247 "缓存命中率"
248 }
249 div { class: "overflow-x-auto",
250 table { class: "w-full text-sm",
251 thead {
252 tr { class: "border-b border-paper-border text-left text-paper-secondary",
253 th { class: "px-4 py-2 font-medium", "缓存" }
254 th { class: "px-4 py-2 font-medium text-right",
255 "条目"
256 }
257 th { class: "px-4 py-2 font-medium text-right",
258 "命中"
259 }
260 th { class: "px-4 py-2 font-medium text-right",
261 "未命中"
262 }
263 th { class: "px-4 py-2 font-medium text-right",
264 "命中率"
265 }
266 }
267 }
268 tbody {
269 for (name, entry_count, hits, misses, rate_pct) in cache_rows.iter() {
270 tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
271 td { class: "px-4 py-2 text-paper-primary",
272 "{name}"
273 }
274 td { class: "px-4 py-2 text-right text-paper-secondary",
275 "{entry_count}"
276 }
277 td { class: "px-4 py-2 text-right text-paper-secondary",
278 "{hits}"
279 }
280 td { class: "px-4 py-2 text-right text-paper-secondary",
281 "{misses}"
282 }
283 td { class: "px-4 py-2 text-right text-paper-primary font-medium",
284 "{rate_pct}"
285 }
286 }
287 }
288 }
289 }
290 }
291 }
292 } else if loading() {
293 DelayedSkeleton {
295 div { class: "space-y-4",
296 div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
298 for _ in 0..4 {
299 div { class: "rounded-2xl bg-paper-entry border border-paper-border p-4 space-y-2",
300 SkeletonBox { class: "h-3 w-16 rounded" }
301 SkeletonBox { class: "h-6 w-24 rounded" }
302 }
303 }
304 }
305 div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
307 for _ in 0..4 {
308 div { class: "rounded-2xl bg-paper-entry border border-paper-border p-4 space-y-2",
309 SkeletonBox { class: "h-3 w-16 rounded" }
310 SkeletonBox { class: "h-6 w-24 rounded" }
311 }
312 }
313 }
314 div { class: "rounded-2xl bg-paper-entry border border-paper-border overflow-hidden",
316 div { class: "px-4 py-3 border-b border-paper-border",
317 SkeletonBox { class: "h-4 w-24 rounded" }
318 }
319 for _ in 0..4 {
320 div { class: "flex justify-between px-4 py-3 border-b border-paper-border last:border-0",
321 SkeletonBox { class: "h-4 w-20 rounded" }
322 SkeletonBox { class: "h-4 w-12 rounded" }
323 }
324 }
325 }
326 }
327 }
328 } else {
329 div { class: "text-paper-secondary py-8", "暂无数据" }
330 }
331 }
332 }
333}