yggdrasil/pages/admin/system/
server_status.rs1use leptos::prelude::*;
8
9use super::{use_polled_status, Meter, Metric, PolledStatus, RefreshControl};
10use crate::api::database::system_status::{get_server_status, ServerStatus};
11use crate::components::skeletons::atoms::SkeletonBox;
12use crate::components::ui::LoadError;
13use crate::utils::format_bytes;
14
15fn format_uptime(secs: u64) -> String {
17 let d = secs / 86400;
18 let h = (secs % 86400) / 3600;
19 let m = (secs % 3600) / 60;
20 if d > 0 {
21 format!("{d} 天 {h} 小时")
22 } else if h > 0 {
23 format!("{h} 小时 {m} 分")
24 } else if m > 0 {
25 format!("{m} 分")
26 } else {
27 format!("{secs} 秒")
28 }
29}
30
31fn percent(part: f64, whole: f64) -> f64 {
33 if whole <= 0.0 {
34 0.0
35 } else {
36 (part / whole * 100.0).clamp(0.0, 100.0)
37 }
38}
39
40fn mem_pct(s: &ServerStatus) -> f64 {
41 percent(s.host.used_memory as f64, s.host.total_memory as f64)
42}
43
44fn disk_pct(s: &ServerStatus) -> f64 {
45 let used = s.host.disk_total.saturating_sub(s.host.disk_available);
46 percent(used as f64, s.host.disk_total as f64)
47}
48
49fn pool_pct(s: &ServerStatus) -> f64 {
50 percent(s.pool_size as f64, s.pool_max_size as f64)
51}
52
53#[component]
55pub(super) fn ServerStatusTab() -> impl IntoView {
56 let PolledStatus {
57 status,
58 loading,
59 failed,
60 refresh_ms,
61 reload,
62 } = use_polled_status(get_server_status, Some(10_000));
63 let ready = Memo::new(move |_| status.with(Option::is_some));
64 let show_error = Memo::new(move |_| failed.get() && !ready.get());
65
66 view! {
67 <RefreshControl refresh_ms loading on_refresh=move |_| reload.run(())/>
68 {move || {
69 if show_error.get() {
70 view! {
71 <LoadError title="服务器状态加载失败" on_retry=move |_| reload.run(())/>
72 }
73 .into_any()
74 } else if ready.get() {
75 view! { <ServerStatusBody status/> }.into_any()
76 } else {
77 view! {
78 <div class="sys-metrics" aria-hidden="true">
79 {(0..8)
80 .map(|_| {
81 view! {
82 <div class="admin-panel sys-metric">
83 <SkeletonBox class="h-3 w-16 rounded"/>
84 <SkeletonBox class="h-6 w-24 rounded mt-2"/>
85 </div>
86 }
87 })
88 .collect_view()}
89 </div>
90 }
91 .into_any()
92 }
93 }}
94 }
95}
96
97#[component]
99fn ServerStatusBody(status: RwSignal<Option<ServerStatus>>) -> impl IntoView {
100 let text = move |f: fn(&ServerStatus) -> String| {
101 Signal::derive(move || status.with(|s| s.as_ref().map(f).unwrap_or_default()))
102 };
103 let pct = move |f: fn(&ServerStatus) -> f64| {
104 Signal::derive(move || status.with(|s| s.as_ref().map(f).unwrap_or_default()))
105 };
106 let caches = move || status.with(|s| s.as_ref().map(|s| s.caches.clone()).unwrap_or_default());
107
108 view! {
109 <div class="sys-metrics">
110 <Metric
111 label="运行时间"
112 value=text(|s| format_uptime(s.uptime_secs))
113 meta=text(|s| format!("主机已运行 {}", format_uptime(s.host.uptime_secs)))
114 />
115 <Metric label="应用 CPU" value=text(|s| format!("{:.1}%", s.host.cpu_usage))>
116 <Meter pct=pct(|s| s.host.cpu_usage as f64)/>
117 </Metric>
118 <Metric
119 label="内存"
120 value=text(|s| format_bytes(s.host.used_memory as i64))
121 meta=text(|s| format!("共 {}", format_bytes(s.host.total_memory as i64)))
122 >
123 <Meter pct=pct(mem_pct)/>
124 </Metric>
125 <Metric
126 label="磁盘"
127 value=text(|s| format!("{:.0}%", disk_pct(s)))
128 meta=text(|s| format!("剩余 {}", format_bytes(s.host.disk_available as i64)))
129 >
130 <Meter pct=pct(disk_pct)/>
131 </Metric>
132 <Metric
133 label="平均负载"
134 value=text(|s| format!("{:.2}", s.host.load_avg_1))
135 meta="1 分钟"
136 />
137 <Metric
138 label="数据库连接池"
139 value=text(|s| format!("{} / {}", s.pool_size, s.pool_max_size))
140 meta=text(|s| format!("空闲 {} · 等待 {}", s.pool_available, s.pool_waiting))
141 >
142 <Meter pct=pct(pool_pct)/>
143 </Metric>
144 <Metric
145 label="活跃会话"
146 value=text(|s| s.active_sessions.to_string())
147 meta="未过期的登录"
148 />
149 <Metric
150 label="宿主系统"
151 value=text(|s| s.host.os_name.clone())
152 meta=text(|s| s.host.kernel_version.clone())
153 small=true
154 />
155 </div>
156 <section class="admin-panel sys-section" aria-labelledby="sys-cache-h">
157 <div class="sys-section-head">
158 <h2 id="sys-cache-h">"内存缓存"</h2>
159 </div>
160 <div class="sys-table-scroll">
161 <table class="admin-table">
162 <thead>
163 <tr>
164 <th>"缓存"</th>
165 <th class="admin-table-num">"条目"</th>
166 <th class="admin-table-num">"命中"</th>
167 <th class="admin-table-num">"未命中"</th>
168 <th>"命中率"</th>
169 </tr>
170 </thead>
171 <tbody>
172 <For
173 each=caches
174 key=|c| (c.name.clone(), c.entry_count, c.hits, c.misses, c.hit_rate.to_bits())
175 children=|c| {
176 view! {
177 <tr>
178 <td class="sys-mono">{c.name.clone()}</td>
179 <td class="admin-table-num">{c.entry_count}</td>
180 <td class="admin-table-num">{c.hits}</td>
181 <td class="admin-table-num">{c.misses}</td>
182 <td>
183 <div class="sys-rate">
184 <Meter pct=c.hit_rate * 100.0 calm=true/>
185 <span>{format!("{:.0}%", c.hit_rate * 100.0)}</span>
186 </div>
187 </td>
188 </tr>
189 }
190 }
191 />
192 </tbody>
193 </table>
194 </div>
195 </section>
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn uptime_reads_in_largest_two_units() {
205 assert_eq!(format_uptime(42), "42 秒");
206 assert_eq!(format_uptime(180), "3 分");
207 assert_eq!(format_uptime(3 * 3600 + 120), "3 小时 2 分");
208 assert_eq!(format_uptime(6 * 86400 + 4 * 3600), "6 天 4 小时");
209 }
210
211 #[test]
212 fn percent_handles_zero_total() {
213 assert_eq!(percent(1.0, 0.0), 0.0);
214 assert_eq!(percent(1.0, 4.0), 25.0);
215 assert_eq!(percent(9.0, 4.0), 100.0);
216 }
217}