yggdrasil/pages/admin/system/
mod.rs1mod backup;
8mod db_status;
9mod export;
10mod server_status;
11mod sql_console;
12
13use backup::BackupTab;
14use db_status::DbStatusTab;
15use dioxus::prelude::*;
16use export::ExportTab;
17use server_status::ServerStatusTab;
18use sql_console::SqlConsoleTab;
19
20use crate::components::ui::FilterTabs;
21
22#[derive(Clone, Copy, PartialEq, Debug)]
24enum SystemTab {
25 DbStatus,
27 ServerStatus,
29 SqlConsole,
31 Export,
33 Backup,
35}
36
37impl SystemTab {
38 fn as_str(&self) -> &'static str {
41 match self {
42 SystemTab::DbStatus => "db_status",
43 SystemTab::ServerStatus => "server_status",
44 SystemTab::SqlConsole => "sql_console",
45 SystemTab::Export => "export",
46 SystemTab::Backup => "backup",
47 }
48 }
49
50 fn from_str(s: &str) -> Result<SystemTab, &'static str> {
53 match s {
54 "db_status" => Ok(SystemTab::DbStatus),
55 "server_status" => Ok(SystemTab::ServerStatus),
56 "sql_console" => Ok(SystemTab::SqlConsole),
57 "export" => Ok(SystemTab::Export),
58 "backup" => Ok(SystemTab::Backup),
59 _ => Err("unknown tab key"),
60 }
61 }
62}
63
64#[component]
66pub fn System() -> Element {
67 let mut active_tab = use_signal(|| SystemTab::DbStatus);
70
71 rsx! {
72 div { class: "w-full max-w-7xl mx-auto space-y-6",
73 div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-6 border-b border-[var(--color-paper-border)] mb-6",
75 div {
76 h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
77 "系统面板"
78 }
79 p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
80 "数据库与服务器诊断"
81 }
82 }
83 }
84
85 FilterTabs {
88 items: vec![
89 ("db_status", "数据库状态"),
90 ("server_status", "服务器状态"),
91 ("sql_console", "SQL 控制台"),
92 ("export", "数据导出"),
93 ("backup", "备份恢复"),
94 ],
95 active_value: active_tab().as_str().to_string(),
96 on_change: move |v: String| {
97 active_tab.set(SystemTab::from_str(&v).unwrap_or(SystemTab::DbStatus));
99 },
100 }
101
102 div { key: "{active_tab().as_str()}",
106 match active_tab() {
107 SystemTab::DbStatus => rsx! {
108 DbStatusTab {}
109 },
110 SystemTab::ServerStatus => rsx! {
111 ServerStatusTab {}
112 },
113 SystemTab::SqlConsole => rsx! {
114 SqlConsoleTab {}
115 },
116 SystemTab::Export => rsx! {
117 ExportTab {}
118 },
119 SystemTab::Backup => rsx! {
120 BackupTab {}
121 },
122 }
123 }
124 }
125 }
126}
127
128pub(super) fn format_bytes(bytes: i64) -> String {
130 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
131 let mut size = bytes as f64;
132 let mut unit = 0;
133 while size.abs() >= 1024.0 && unit < UNITS.len() - 1 {
134 size /= 1024.0;
135 unit += 1;
136 }
137 if unit == 0 {
138 format!("{} {}", bytes, UNITS[0])
139 } else {
140 format!("{:.2} {}", size, UNITS[unit])
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::SystemTab;
147
148 #[test]
149 fn as_str_roundtrips_all_variants() {
150 for tab in [
152 SystemTab::DbStatus,
153 SystemTab::ServerStatus,
154 SystemTab::SqlConsole,
155 SystemTab::Export,
156 SystemTab::Backup,
157 ] {
158 let s = tab.as_str();
159 assert_eq!(
160 SystemTab::from_str(s),
161 Ok(tab),
162 "roundtrip failed for {tab:?}"
163 );
164 }
165 }
166
167 #[test]
168 fn as_str_returns_stable_keys() {
169 assert_eq!(SystemTab::DbStatus.as_str(), "db_status");
171 assert_eq!(SystemTab::ServerStatus.as_str(), "server_status");
172 assert_eq!(SystemTab::SqlConsole.as_str(), "sql_console");
173 assert_eq!(SystemTab::Export.as_str(), "export");
174 assert_eq!(SystemTab::Backup.as_str(), "backup");
175 }
176
177 #[test]
178 fn from_str_rejects_unknown_and_empty() {
179 assert!(SystemTab::from_str("nonsense").is_err());
180 assert!(SystemTab::from_str("").is_err());
181 assert!(SystemTab::from_str("DbStatus").is_err());
183 }
184}