From 08bca5a1295a418302a730cc6116791afa7811a7 Mon Sep 17 00:00:00 2001 From: houseme Date: Tue, 18 Feb 2025 01:29:27 +0800 Subject: [PATCH] Add new home views to rustfs-gui (#222) - Create `Home` component in `components/Home.rs`. - Create `Navbar` component in `components/navbar.rs`. - Create `HomeView` component in `views/home.rs`. - Update `views/mod.rs` to include new views. - Update routing in `main.rs` to include new routes for `HomeView`. --- Cargo.lock | 27 ++ Cargo.toml | 1 + cli/rustfs-gui/Cargo.toml | 2 + cli/rustfs-gui/src/components/home.rs | 315 ++++++++++++++++++++++++ cli/rustfs-gui/src/components/mod.rs | 4 + cli/rustfs-gui/src/components/navbar.rs | 59 +++++ cli/rustfs-gui/src/main.rs | 64 ++++- cli/rustfs-gui/src/utils/mod.rs | 1 + cli/rustfs-gui/src/views/home.rs | 9 + cli/rustfs-gui/src/views/mod.rs | 2 + 10 files changed, 478 insertions(+), 6 deletions(-) create mode 100644 cli/rustfs-gui/src/components/home.rs create mode 100644 cli/rustfs-gui/src/components/navbar.rs create mode 100644 cli/rustfs-gui/src/views/home.rs diff --git a/Cargo.lock b/Cargo.lock index 06919cde..d1dc017d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5438,6 +5438,8 @@ dependencies = [ "serde", "serde_json", "tokio", + "tracing-appender", + "tracing-subscriber", "zip", ] @@ -6682,6 +6684,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.28" @@ -6724,6 +6738,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" @@ -6734,6 +6758,8 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", @@ -6741,6 +6767,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5cbc5170..eed30992 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ tower = { version = "0.5.2", features = ["timeout"] } tracing = "0.1.41" tracing-error = "0.2.1" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "time"] } +tracing-appender = "0.2.3" transform-stream = "0.3.1" url = "2.5.4" uuid = { version = "1.12.1", features = [ diff --git a/cli/rustfs-gui/Cargo.toml b/cli/rustfs-gui/Cargo.toml index 0758270d..1d2675b0 100644 --- a/cli/rustfs-gui/Cargo.toml +++ b/cli/rustfs-gui/Cargo.toml @@ -17,6 +17,8 @@ rfd = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["io-util", "net", "process", "sync"] } +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter", "tracing-log", "time", "local-time", "json"] } +tracing-appender = { workspace = true } zip = { workspace = true } diff --git a/cli/rustfs-gui/src/components/home.rs b/cli/rustfs-gui/src/components/home.rs new file mode 100644 index 00000000..a741d5c7 --- /dev/null +++ b/cli/rustfs-gui/src/components/home.rs @@ -0,0 +1,315 @@ +use crate::components::navbar::LoadingSpinner; +use crate::utils::{RustFSConfig, ServiceManager}; +use crate::Route; +use chrono::Datelike; +use dioxus::logger::tracing::debug; +use dioxus::prelude::*; +use std::time::Duration; + +const HEADER_LOGO: Asset = asset!("/assets/rustfs-logo.svg"); +const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css"); + +/// Define the state of the service +#[derive(PartialEq, Debug, Clone)] +enum ServiceState { + Start, + Stop, +} + +/// Define the Home component +/// The Home component is the main component of the application +/// It is responsible for starting and stopping the service +/// It also displays the service status and provides a button to toggle the service +/// The Home component also displays the footer of the application +/// The footer contains links to the official site, documentation, GitHub, and license +/// The footer also displays the version of the application +/// The Home component also contains a button to change the theme of the application +/// The Home component also contains a button to go to the settings page +#[component] +pub fn Home() -> Element { + #[allow(clippy::redundant_closure)] + let service = use_signal(|| ServiceManager::new()); + let conf = RustFSConfig::load().unwrap_or_else(|e| { + ServiceManager::show_error(&format!("加载配置失败:{}", e)); + RustFSConfig::default() + }); + + debug!("loaded configurations:{:?}", conf); + let config = use_signal(|| conf.clone()); + + use dioxus_router::prelude::Link; + use document::{Meta, Stylesheet, Title}; + let mut service_state = use_signal(|| ServiceState::Start); + // Create a periodic check on the effect of the service status + use_effect(move || { + spawn(async move { + loop { + if let Some(pid) = ServiceManager::check_service_status().await { + debug!("service_running true pid: {:?}", pid); + service_state.set(ServiceState::Stop); + } else { + debug!("service_running true pid: 0"); + service_state.set(ServiceState::Start); + } + tokio::time::sleep(Duration::from_secs(2)).await; + } + }); + }); + debug!("project start service_state: {:?}", service_state.read()); + // Use 'use_signal' to manage service status + let mut loading = use_signal(|| false); + let mut start_service = move |_| { + let service = service; + let config = config.read().clone(); + let mut service_state = service_state; + // set the loading status + loading.set(true); + debug!("stop loading_state: {:?}", loading.read()); + spawn(async move { + match service.read().start(config).await { + Ok(result) => { + if result.success { + let duration = result.end_time - result.start_time; + debug!("The service starts successfully and takes a long time:{}ms", duration.num_milliseconds()); + service_state.set(ServiceState::Stop); + } else { + ServiceManager::show_error(&result.message); + service_state.set(ServiceState::Start); + } + } + Err(e) => { + ServiceManager::show_error(&format!("服务启动失败:{}", e)); + } + } + // Only set loading to false when it's actually done + loading.set(false); + debug!("start loading_state: {:?}", loading.read()); + }); + }; + + let mut stop_service = move |_| { + let service = service; + let mut service_state = service_state; + // set the loading status + loading.set(true); + spawn(async move { + match service.read().stop().await { + Ok(result) => { + if result.success { + let duration = result.end_time - result.start_time; + debug!("The service stops successfully and takes a long time:{}ms", duration.num_milliseconds()); + service_state.set(ServiceState::Start); + } else { + ServiceManager::show_error(&result.message); + } + } + Err(e) => { + ServiceManager::show_error(&format!("服务停止失败:{}", e)); + } + } + debug!("service_state: {:?}", service_state.read()); + // Only set loading to false when it's actually done + loading.set(false); + debug!("stop loading_state: {:?}", loading.read()); + }); + }; + + // Toggle the state when the button is clicked + let toggle_service = { + let mut service_state = service_state; + debug!("toggle_service service_state: {:?}", service_state.read()); + move |_| { + if service_state.read().eq(&ServiceState::Stop) { + // If the service status is started, you need to run a command to stop the service + stop_service(()); + service_state.set(ServiceState::Start); + } else { + start_service(()); + service_state.set(ServiceState::Stop); + } + } + }; + + // Define dynamic styles based on state + let button_class = if service_state.read().eq(&ServiceState::Start) { + "bg-[#111827] hover:bg-[#1f2937] text-white px-4 py-2 rounded-md flex items-center space-x-2" + } else { + "bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md flex items-center space-x-2" + }; + + rsx! { + // The Stylesheet component inserts a style link into the head of the document + Stylesheet {href: TAILWIND_CSS,} + Title { "RustFS APP" } + Meta { + name: "description", + content: "RustFS RustFS 用热门安全的 Rust 语言开发,兼容 S3 协议。适用于 AI/ML 及海量数据存储、大数据、互联网、工业和保密存储等全部场景。近乎免费使用。遵循 Apache 2 协议,支持国产保密设备和系统。", + } + div { class: "min-h-screen flex flex-col items-center bg-white", + div { class: "absolute top-4 right-6 flex space-x-2", + // change theme + button { class: "p-2 hover:bg-gray-100 rounded-lg", ChangeThemeButton {} } + // setting button + Link { + class: "p-2 hover:bg-gray-100 rounded-lg", + to: Route::HomeViews {}, + SettingButton {} + } + } + main { class: "flex-1 flex flex-col items-center justify-center space-y-6 p-4", + div { class: "w-24 h-24 bg-gray-900 rounded-full flex items-center justify-center", + img { alt: "Logo", class: "w-16 h-16", src: HEADER_LOGO } + } + div { class: "text-gray-600", + "Service is running on " + span { class: "text-blue-600", " 127.0.0.1:9000 " } + } + LoadingSpinner { + loading: loading.read().to_owned(), + text: "服务处理中...", + } + button { class: button_class, onclick: toggle_service, + svg { + class: "h-4 w-4", + fill: "none", + stroke: "currentColor", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + if service_state.read().eq(&ServiceState::Start) { + path { + d: "M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2", + } + path { + d: "M21 12a9 9 0 11-18 0 9 9 0 0118 0z", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2", + } + } else { + path { + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2", + d: "M21 12a9 9 0 11-18 0 9 9 0 0118 0z", + } + path { + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2", + d: "M9 10h6v4H9z", + } + } + } + span { id: "serviceStatus", + if service_state.read().eq(&ServiceState::Start) { + "Start service" + } else { + "Stop service" + } + } + } + } + Footer { version: "v1.0.0".to_string() } + } + } +} + +#[component] +pub fn Footer(version: String) -> Element { + let now = chrono::Local::now(); + let year = now.naive_local().year(); + rsx! { + footer { class: "w-full py-6 flex flex-col items-center space-y-4 mb-6", + nav { class: "flex space-x-4 text-gray-600", + a { class: "hover:text-gray-900", href: "https://rustfs.com", "Official Site" } + a { + class: "hover:text-gray-900", + href: "https://rustfs.com/docs", + "Documentation" + } + a { + class: "hover:text-gray-900", + href: "https://github.com/rustfs/rustfs", + "GitHub" + } + a { + class: "hover:text-gray-900", + href: "https://rustfs.com/docs/license/", + "License" + } + a { class: "hover:text-gray-900", href: "#", "Sponsors" } + } + div { class: "text-gray-500 text-sm", " © rustfs.com {year}, All rights reserved." } + div { class: "text-gray-400 text-sm mb-8", " version {version} " } + } + } +} + +#[component] +pub fn GoBackButtons() -> Element { + rsx! { + button { + class: "p-2 hover:bg-gray-100 rounded-lg", + "onclick": "window.history.back()", + "Back to the Past" + } + } +} + +#[component] +pub fn GoForwardButtons() -> Element { + rsx! { + button { + class: "p-2 hover:bg-gray-100 rounded-lg", + "onclick": "window.history.forward()", + "Back to the Future" + } + } +} + +#[component] +pub fn ChangeThemeButton() -> Element { + rsx! { + svg { + class: "h-6 w-6 text-gray-600", + fill: "none", + stroke: "currentColor", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + path { + d: "M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2", + } + } + } +} + +#[component] +pub fn SettingButton() -> Element { + rsx! { + svg { + class: "h-6 w-6 text-gray-600", + fill: "none", + stroke: "currentColor", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + path { + d: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2", + } + path { + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2", + } + } + } +} diff --git a/cli/rustfs-gui/src/components/mod.rs b/cli/rustfs-gui/src/components/mod.rs index e69de29b..354c6bfc 100644 --- a/cli/rustfs-gui/src/components/mod.rs +++ b/cli/rustfs-gui/src/components/mod.rs @@ -0,0 +1,4 @@ +mod home; +pub use home::Home; +mod navbar; +pub use navbar::Navbar; diff --git a/cli/rustfs-gui/src/components/navbar.rs b/cli/rustfs-gui/src/components/navbar.rs new file mode 100644 index 00000000..a9445f35 --- /dev/null +++ b/cli/rustfs-gui/src/components/navbar.rs @@ -0,0 +1,59 @@ +use crate::Route; +use dioxus::logger::tracing::debug; +use dioxus::prelude::*; + +const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); + +#[component] +pub fn Navbar() -> Element { + rsx! { + document::Link { rel: "stylesheet", href: NAVBAR_CSS } + + div { id: "navbar", class: "hidden", style: "display: none;", + Link { to: Route::HomeViews {}, "Home" } + } + + Outlet:: {} + } +} + +#[derive(Props, PartialEq, Debug, Clone)] +pub struct LoadingSpinnerProps { + #[props(default = true)] + loading: bool, + #[props(default = "正在处理中...")] + text: &'static str, +} + +#[component] +pub fn LoadingSpinner(props: LoadingSpinnerProps) -> Element { + debug!("loading: {}", props.loading); + if !props.loading { + debug!("LoadingSpinner false loading: {}", props.loading); + return rsx! {}; + } + rsx! { + div { class: "flex items-center justify-center z-10", + svg { + class: "animate-spin h-5 w-5 text-blue-500", + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + view_box: "0 0 24 24", + circle { + class: "opacity-25", + cx: "12", + cy: "12", + r: "10", + stroke: "currentColor", + stroke_width: "4", + } + path { + class: "opacity-75", + fill: "currentColor", + d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z", + } + } + span { class: "ml-2 text-gray-600", "{props.text}" } + } + } +} diff --git a/cli/rustfs-gui/src/main.rs b/cli/rustfs-gui/src/main.rs index bf783b82..53236943 100644 --- a/cli/rustfs-gui/src/main.rs +++ b/cli/rustfs-gui/src/main.rs @@ -1,17 +1,69 @@ +use components::Navbar; +use dioxus::logger::tracing::debug; +use dioxus::prelude::*; +use tracing_appender::rolling::{RollingFileAppender, Rotation}; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; +use views::HomeViews; + mod components; mod utils; mod views; -use dioxus::prelude::*; - fn main() { + init_logger(); dioxus::launch(App); } +#[derive(Debug, Clone, Routable, PartialEq)] +#[rustfmt::skip] +enum Route { + #[layout(Navbar)] + #[route("/")] + HomeViews {}, +} + +const FAVICON: Asset = asset!("/assets/favicon.ico"); +const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css"); + #[component] fn App() -> Element { - // The use signal hook runs once when the component is created and then returns the current value every run after the first - let name = use_signal(|| "RustFS"); - - rsx! { "hello {name}!" } + // Build cool things ✌️ + use document::{Link, Title}; + debug!("App rendered"); + rsx! { + // Global app resources + Link { rel: "icon", href: FAVICON } + Link { rel: "stylesheet", href: TAILWIND_CSS } + // Script { src: "https://cdn.tailwindcss.com" } + Title { "RustFS" } + Router:: {} + } +} + +fn init_logger() { + // configuring rolling logs rolling by day + let home_dir = dirs::home_dir().expect("无法获取用户目录"); + let rustfs_dir = home_dir.join("rustfs"); + let logs_dir = rustfs_dir.join("logs"); + let file_appender = RollingFileAppender::builder() + .rotation(Rotation::DAILY) // rotate log files once every hour + .filename_prefix("rustfs-cli") // log file names will be prefixed with `myapp.` + .filename_suffix("log") // log file names will be suffixed with `.log` + .build(logs_dir) // try to build an appender that stores log files in `/ var/ log` + .expect("initializing rolling file appender failed"); + // non-blocking writer for improved performance + let (non_blocking_file, _guard) = tracing_appender::non_blocking(file_appender); + + // console output layer + let console_layer = fmt::layer().with_writer(std::io::stdout).with_ansi(true); // enable colors in the console + + // file output layer + let file_layer = fmt::layer().with_writer(non_blocking_file).with_ansi(false); // disable colors in the file + + // Combine all tiers and initialize global subscribers + tracing_subscriber::registry() + .with(console_layer) + .with(file_layer) + .with(tracing_subscriber::EnvFilter::new("info")) // filter the log level by environment variables + .init(); } diff --git a/cli/rustfs-gui/src/utils/mod.rs b/cli/rustfs-gui/src/utils/mod.rs index 73435da7..8fee712d 100644 --- a/cli/rustfs-gui/src/utils/mod.rs +++ b/cli/rustfs-gui/src/utils/mod.rs @@ -2,3 +2,4 @@ mod config; mod helper; pub use config::RustFSConfig; +pub use helper::ServiceManager; diff --git a/cli/rustfs-gui/src/views/home.rs b/cli/rustfs-gui/src/views/home.rs new file mode 100644 index 00000000..d669f9e6 --- /dev/null +++ b/cli/rustfs-gui/src/views/home.rs @@ -0,0 +1,9 @@ +use crate::components::Home; +use dioxus::prelude::*; + +#[component] +pub fn HomeViews() -> Element { + rsx! { + Home {} + } +} diff --git a/cli/rustfs-gui/src/views/mod.rs b/cli/rustfs-gui/src/views/mod.rs index e69de29b..f1756106 100644 --- a/cli/rustfs-gui/src/views/mod.rs +++ b/cli/rustfs-gui/src/views/mod.rs @@ -0,0 +1,2 @@ +mod home; +pub use home::HomeViews;