commit ee2423dd19c839476213970be6dd7c581e58c48f Author: accusys Date: Sun Mar 29 15:47:09 2026 +0800 Complete Slint UI mimicking Java Swing frmMain diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..d4354f6 Binary files /dev/null and b/.DS_Store differ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5e8a53f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "raidguard_x_gui_client" +version = "0.1.0" +edition = "2021" +authors = ["Accusys"] +description = "RAIDGuard X GUI Client - Slint-based cross-platform GUI" + +[lib] +name = "raidguard_x_gui_client" +crate-type = ["lib", "cdylib", "staticlib"] +path = "src/lib.rs" + +[dependencies] +slint = "1.7" +tokio = { version = "1.42", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.10", features = ["v4"] } +thiserror = "2.0" +parking_lot = "0.12" +anyhow = "1.0" +chrono = "0.4" + +[[bin]] +name = "raidguard-client" +path = "src/main.rs" + +[build-dependencies] +slint-build = "1.7" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..2f05231 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + slint_build::compile("ui/main_window.slint").unwrap(); +} diff --git a/icons/ball-green.png b/icons/ball-green.png new file mode 100644 index 0000000..42d962d Binary files /dev/null and b/icons/ball-green.png differ diff --git a/icons/disk-1.png b/icons/disk-1.png new file mode 100644 index 0000000..059b23b Binary files /dev/null and b/icons/disk-1.png differ diff --git a/icons/func-settings.png b/icons/func-settings.png new file mode 100644 index 0000000..4120b9e Binary files /dev/null and b/icons/func-settings.png differ diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..1ded98b --- /dev/null +++ b/src/app.rs @@ -0,0 +1,272 @@ +//! Application state and integration with Slint + +use std::sync::Arc; +use parking_lot::RwLock; +use anyhow::Result; +use serde::Deserialize; + +use crate::protocol::client::ServerClient; +use crate::models::{Controller, RaidArray, Disk, Event}; + +pub struct AppState { + pub client: Arc>>, + pub controllers: Arc>>, + pub raids: Arc>>, + pub disks: Arc>>, + pub events: Arc>>, + pub selected_controller: Arc>>, + pub selected_level: Arc>, + pub connected: Arc>, +} + +#[derive(Debug, Clone)] +pub struct Pagination { + pub page: i32, + pub page_size: i32, + pub total_count: i32, + pub total_pages: i32, + pub has_next: bool, + pub has_prev: bool, +} + +impl<'de> Deserialize<'de> for Pagination { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + #[derive(Deserialize)] + struct Inner { + page: i32, + page_size: i32, + total_count: i32, + total_pages: i32, + has_next: bool, + has_prev: bool, + } + let inner = Inner::deserialize(deserializer)?; + Ok(Pagination { + page: inner.page, + page_size: inner.page_size, + total_count: inner.total_count, + total_pages: inner.total_pages, + has_next: inner.has_next, + has_prev: inner.has_prev, + }) + } +} + +impl AppState { + pub fn new() -> Self { + Self { + client: Arc::new(RwLock::new(None)), + controllers: Arc::new(RwLock::new(Vec::new())), + raids: Arc::new(RwLock::new(Vec::new())), + disks: Arc::new(RwLock::new(Vec::new())), + events: Arc::new(RwLock::new(Vec::new())), + selected_controller: Arc::new(RwLock::new(None)), + selected_level: Arc::new(RwLock::new("all".to_string())), + connected: Arc::new(RwLock::new(false)), + } + } + + pub async fn connect(&self, addr: &str) -> Result<()> { + tracing::info!("=== AppState::connect() called with addr: {} ===", addr); + let client = ServerClient::connect(addr).await?; + tracing::info!("=== ServerClient connected, storing in AppState ==="); + *self.client.write() = Some(client); + *self.connected.write() = true; + tracing::info!("=== AppState connected flag set to true ==="); + Ok(()) + } + + pub async fn refresh(&self) -> Result<()> { + tracing::info!("=== refresh() called ==="); + + // Get client - clone it so we don't interfere with the stored one + let client_opt = self.client.read().clone(); + tracing::info!("Got client option: {:?}", client_opt.is_some()); + + let client = match client_opt { + Some(c) => c, + None => return Err(anyhow::anyhow!("Not connected")), + }; + + tracing::info!("Fetching controllers..."); + // Fetch all data + let controllers_data = client.get_controllers().await?; + tracing::info!("Got {} controllers from server", controllers_data.len()); + + let mut controllers = Vec::new(); + for v in controllers_data { + if let Some(c) = Controller::from_json(&v) { + controllers.push(c); + } + } + tracing::info!("Parsed {} controllers", controllers.len()); + *self.controllers.write() = controllers; + + tracing::info!("Fetching raids..."); + let raids_data = client.get_raids(None).await?; + tracing::info!("Got {} raids from server", raids_data.len()); + + let mut raids = Vec::new(); + for v in raids_data { + if let Some(r) = RaidArray::from_json(&v) { + raids.push(r); + } + } + tracing::info!("Parsed {} raids", raids.len()); + *self.raids.write() = raids; + + tracing::info!("Fetching disks..."); + let disks_data = client.get_disks(None).await?; + tracing::info!("Got {} disks from server", disks_data.len()); + let mut disks = Vec::new(); + for v in disks_data { + if let Some(d) = Disk::from_json(&v) { + disks.push(d); + } + } + *self.disks.write() = disks; + + let events_data = client.get_events(None, Some(50)).await?; + let mut events = Vec::new(); + for v in events_data { + if let Some(e) = Event::from_json(&v) { + events.push(e); + } + } + *self.events.write() = events; + + Ok(()) + } + + pub fn is_connected(&self) -> bool { + *self.connected.read() + } + + pub fn get_controllers(&self) -> Vec { + self.controllers.read().clone() + } + + pub fn get_raids(&self) -> Vec { + self.raids.read().clone() + } + + pub fn get_disks(&self) -> Vec { + self.disks.read().clone() + } + + pub fn get_events(&self) -> Vec { + self.events.read().clone() + } + + pub async fn load_events_page(&self, page: i32, page_size: i32) -> Result> { + let client = self.client.read().clone(); + let client = match client { + Some(c) => c, + None => return Err(anyhow::anyhow!("Not connected")), + }; + + // Get selected level filter + let level = self.selected_level.read().clone(); + let level_filter = if level == "all" { None } else { Some(level) }; + + // Fetch events with pagination + let response = client.get_events_paginated(page, page_size, level_filter).await?; + + let events_data = response.events; + let pagination = response.pagination; + + let mut events = Vec::new(); + for v in events_data { + if let Some(e) = Event::from_json(&v) { + events.push(e); + } + } + *self.events.write() = events; + + Ok(Some(pagination)) + } + + pub async fn set_selected_level(&self, level: String) -> Result<()> { + *self.selected_level.write() = level; + Ok(()) + } + + // RAID Operations + pub async fn create_raid(&self, name: &str, level: u32, disks: Vec) -> Result { + let client = self.client.read().clone(); + let client = match client { + Some(c) => c, + None => return Err(anyhow::anyhow!("Not connected")), + }; + client.create_raid(name, level, disks).await + } + + pub async fn delete_raid(&self, raid_id: u32) -> Result { + let client = self.client.read().clone(); + let client = match client { + Some(c) => c, + None => return Err(anyhow::anyhow!("Not connected")), + }; + client.delete_raid(raid_id).await + } + + pub async fn rebuild_raid(&self, raid_id: u32) -> Result { + let client = self.client.read().clone(); + let client = match client { + Some(c) => c, + None => return Err(anyhow::anyhow!("Not connected")), + }; + client.rebuild_raid(raid_id).await + } + + pub async fn initialize_disk(&self, disk_id: u32) -> Result { + let client = self.client.read().clone(); + let client = match client { + Some(c) => c, + None => return Err(anyhow::anyhow!("Not connected")), + }; + client.initialize_disk(disk_id).await + } + + pub async fn locate_disk(&self, disk_id: u32) -> Result { + let client = self.client.read().clone(); + let client = match client { + Some(c) => c, + None => return Err(anyhow::anyhow!("Not connected")), + }; + client.locate_disk(disk_id).await + } + + pub async fn remove_disk(&self, disk_id: u32, force: bool) -> Result { + let client = self.client.read().clone(); + let client = match client { + Some(c) => c, + None => return Err(anyhow::anyhow!("Not connected")), + }; + client.remove_disk(disk_id, force).await + } + + pub async fn commit_config(&self) -> Result { + let client = self.client.read().clone(); + let client = match client { + Some(c) => c, + None => return Err(anyhow::anyhow!("Not connected")), + }; + client.commit_config().await + } + + pub async fn connect_internal(addr: &str) -> Result<()> { + let client = crate::protocol::client::ServerClient::connect(addr).await?; + let mut this = Self::new(); + *this.client.write() = Some(client); + *this.connected.write() = true; + Ok(()) + } +} + +impl Default for AppState { + fn default() -> Self { + Self::new() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..69a7d20 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +//! RAIDGuard X GUI Client +//! +//! A cross-platform GUI client using Slint and TCP connection to the GUI server. + +pub mod protocol; +pub mod models; +pub mod app; + +pub use app::AppState; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0948c59 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,467 @@ +//! RAIDGuard X GUI Client - Main entry point + +use anyhow::Result; +use std::sync::{Arc, Mutex}; +use slint::{ComponentHandle, SharedString}; +use raidguard_x_gui_client::{AppState, models::{Controller, RaidArray, Disk, Event}}; + +slint::include_modules!(); + +fn to_s(s: &str) -> SharedString { + s.into() +} + +fn update_ui_from_state(app: &AppWindow, state: &AppState) { + let controllers = state.get_controllers(); + let raids = state.get_raids(); + let disks = state.get_disks(); + let connected = state.is_connected(); + let selected_idx = app.get_selected_controller_index() as usize; + + tracing::info!("update_ui_from_state: {} controllers, {} raids, {} disks, selected={}", controllers.len(), raids.len(), disks.len(), selected_idx); + + app.set_connected(connected); + app.set_status_text(to_s(if connected { "Connected" } else { "Disconnected" })); + app.set_controller_count(controllers.len() as i32); + app.set_raid_count(raids.len() as i32); + app.set_disk_count(disks.len() as i32); + + tracing::info!("Setting counts: controller={}, raid={}, disk={}", controllers.len(), raids.len(), disks.len()); + + // Controller 1 + if let Some(c) = controllers.get(0) { + tracing::info!("Setting controller 1: {} {} {}", c.hostname, c.ip, c.status); + app.set_ctrl1_name(to_s(&c.hostname)); + app.set_ctrl1_ip(to_s(&c.ip)); + app.set_ctrl1_status(to_s(&c.status)); + app.set_ctrl1_model(to_s(&c.model)); + app.set_ctrl1_firmware(to_s(&c.firmware_version)); + } else { + tracing::info!("No controller data!"); + } + + // Controller 2 + if let Some(c) = controllers.get(1) { + tracing::info!("Setting controller 2: {} {} {}", c.hostname, c.ip, c.status); + app.set_ctrl2_name(to_s(&c.hostname)); + app.set_ctrl2_ip(to_s(&c.ip)); + app.set_ctrl2_status(to_s(&c.status)); + app.set_ctrl2_model(to_s(&c.model)); + app.set_ctrl2_firmware(to_s(&c.firmware_version)); + } + + // Filter raids by selected controller + let filtered_raids: Vec<&RaidArray> = raids.iter().filter(|r| r.controller_id as usize == selected_idx).collect(); + tracing::info!("Selected controller {} has {} raids", selected_idx, filtered_raids.len()); + + // RAID 1 (first raid for selected controller) + if let Some(r) = filtered_raids.get(0) { + tracing::info!("Setting RAID 1: {} {} {}", r.name, r.raid_level, r.status); + app.set_raid1_name(to_s(&r.name)); + app.set_raid1_level(to_s(&r.raid_level)); + app.set_raid1_status(to_s(&r.status)); + app.set_raid1_capacity(format!("{:.1} TB", r.total_capacity_tb).into()); + app.set_raid1_usage(format!("{:.0}%", r.usage_percent()).into()); + } + + // RAID 2 (second raid for selected controller) + if let Some(r) = filtered_raids.get(1) { + tracing::info!("Setting RAID 2: {} {} {}", r.name, r.raid_level, r.status); + app.set_raid2_name(to_s(&r.name)); + app.set_raid2_level(to_s(&r.raid_level)); + app.set_raid2_status(to_s(&r.status)); + app.set_raid2_capacity(format!("{:.1} TB", r.total_capacity_tb).into()); + app.set_raid2_usage(format!("{:.0}%", r.usage_percent()).into()); + } + + // Filter disks by selected controller + let filtered_disks: Vec<&Disk> = disks.iter().filter(|d| d.controller_id as usize == selected_idx).collect(); + tracing::info!("Selected controller {} has {} disks", selected_idx, filtered_disks.len()); + + // Disks + for (i, d) in filtered_disks.iter().enumerate() { + let slot = format!("Enclosure {} Slot {}", d.enclosure, d.slot); + let cap = format!("{:.1} TB", d.capacity_tb); + tracing::info!("Setting disk {}: {} {} {}", i, slot, d.model, d.status); + match i { + 0 => { + app.set_disk1_loc(to_s(&slot)); + app.set_disk1_model(to_s(&d.model)); + app.set_disk1_status(to_s(&d.status)); + app.set_disk1_capacity(cap.into()); + } + 1 => { + app.set_disk2_loc(to_s(&slot)); + app.set_disk2_model(to_s(&d.model)); + app.set_disk2_status(to_s(&d.status)); + app.set_disk2_capacity(cap.into()); + } + 2 => { + app.set_disk3_loc(to_s(&slot)); + app.set_disk3_model(to_s(&d.model)); + app.set_disk3_status(to_s(&d.status)); + app.set_disk3_capacity(cap.into()); + } + 3 => { + app.set_disk4_loc(to_s(&slot)); + app.set_disk4_model(to_s(&d.model)); + app.set_disk4_status(to_s(&d.status)); + app.set_disk4_capacity(cap.into()); + } + _ => {} + } + } + + // Events + let events = state.get_events(); + for (i, e) in events.iter().enumerate() { + match i { + 0 => { + app.set_evtime1(to_s(&e.formatted_time())); + app.set_evlevel1(to_s(&e.level)); + app.set_evmsg1(to_s(&e.message)); + app.set_evsource1(to_s(&e.event_type)); + } + 1 => { + app.set_evtime2(to_s(&e.formatted_time())); + app.set_evlevel2(to_s(&e.level)); + app.set_evmsg2(to_s(&e.message)); + app.set_evsource2(to_s(&e.event_type)); + } + 2 => { + app.set_evtime3(to_s(&e.formatted_time())); + app.set_evlevel3(to_s(&e.level)); + app.set_evmsg3(to_s(&e.message)); + app.set_evsource3(to_s(&e.event_type)); + } + 3 => { + app.set_evtime4(to_s(&e.formatted_time())); + app.set_evlevel4(to_s(&e.level)); + app.set_evmsg4(to_s(&e.message)); + app.set_evsource4(to_s(&e.event_type)); + } + 4 => { + app.set_evtime5(to_s(&e.formatted_time())); + app.set_evlevel5(to_s(&e.level)); + app.set_evmsg5(to_s(&e.message)); + app.set_evsource5(to_s(&e.event_type)); + } + _ => {} + } + } +} + +fn main() -> Result<()> { + std::panic::set_hook(Box::new(|panic_info| { + eprintln!("PANIC: {}", panic_info); + })); + + let file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("/tmp/raidguard_client.log") + .unwrap(); + + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::DEBUG.into()), + ) + .with_ansi(false) + .init(); + + // Also log to file directly + let mut log_file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("/tmp/raidguard_client.log") + .unwrap(); + use std::io::Write; + writeln!(log_file, "=== RAIDGuard X GUI Client Started ===").unwrap(); + drop(log_file); + + tracing::info!("=== RAIDGuard X GUI Client ==="); + + let app = AppWindow::new()?; + app.show().ok(); + let app_state = Arc::new(Mutex::new(AppState::new())); + + // Connect to server + let state = app_state.clone(); + let app_handle = app.as_weak(); + app.on_connect_server(move || { + let state = state.clone(); + let app_handle = app_handle.clone(); + + // Spawn async task + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + tracing::info!("=== Connect button clicked ==="); + + // First connect + let connect_result = { + let s = match state.lock() { + Ok(s) => s, + Err(_) => return, + }; + tracing::info!("Calling s.connect()..."); + s.connect("127.0.0.1:8923").await + }; + + match connect_result { + Ok(_) => { + tracing::info!("=== Connected to server! ==="); + + // Wait a bit to avoid lock contention + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + // Refresh data + { + let s = match state.lock() { + Ok(s) => s, + Err(_) => return, + }; + tracing::info!("Calling s.refresh()..."); + let _ = s.refresh().await; + } + + tracing::info!("=== Refresh complete ==="); + + // Need to invoke UI update from the main thread + let app_handle = app_handle.clone(); + let state = state.clone(); + slint::invoke_from_event_loop(move || { + tracing::info!("=== Updating UI in main thread ==="); + if let Some(a) = app_handle.upgrade() { + tracing::info!("=== Upgrading app handle ==="); + if let Ok(s) = state.lock() { + tracing::info!("=== Got state lock ==="); + a.set_connected(true); + a.set_status_text(to_s("Connected")); + update_ui_from_state(&a, &s); + tracing::info!("=== UI update complete ==="); + } + } + }).ok(); + } + Err(e) => { + tracing::error!("Failed to connect: {}", e); + } + } + }); + }); + }); + + // Refresh data + let state = app_state.clone(); + let app_handle = app.as_weak(); + app.on_refresh_data(move || { + let state = state.clone(); + let app_handle = app_handle.clone(); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + if let Ok(s) = state.lock() { + if s.is_connected() { + if let Err(e) = s.refresh().await { + tracing::error!("Failed to refresh: {}", e); + } + if let Some(a) = app_handle.upgrade() { + update_ui_from_state(&a, &s); + } + } + } + }); + }); + + // Toggle auto refresh + app.on_toggle_auto_refresh(move || { + tracing::debug!("Toggle auto refresh"); + }); + + // Load events page + let state = app_state.clone(); + let app_handle = app.as_weak(); + app.on_load_events_page(move |page| { + let state = state.clone(); + let app_handle = app_handle.clone(); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + if let Ok(s) = state.lock() { + let _ = s.load_events_page(page, 10).await; + if let Some(a) = app_handle.upgrade() { + let events = s.get_events(); + for (i, e) in events.iter().enumerate() { + match i { + 0 => { a.set_evtime1(to_s(&e.formatted_time())); a.set_evlevel1(to_s(&e.level)); a.set_evmsg1(to_s(&e.message)); } + 1 => { a.set_evtime2(to_s(&e.formatted_time())); a.set_evlevel2(to_s(&e.level)); a.set_evmsg2(to_s(&e.message)); } + 2 => { a.set_evtime3(to_s(&e.formatted_time())); a.set_evlevel3(to_s(&e.level)); a.set_evmsg3(to_s(&e.message)); } + 3 => { a.set_evtime4(to_s(&e.formatted_time())); a.set_evlevel4(to_s(&e.level)); a.set_evmsg4(to_s(&e.message)); } + 4 => { a.set_evtime5(to_s(&e.formatted_time())); a.set_evlevel5(to_s(&e.level)); a.set_evmsg5(to_s(&e.message)); } + _ => {} + } + } + } + } + }); + }); + + // Show event details + let state = app_state.clone(); + let app_handle = app.as_weak(); + app.on_show_event_details(move |index| { + if let Ok(s) = state.lock() { + let events = s.get_events(); + if let Some(event) = events.get(index as usize) { + if let Some(a) = app_handle.upgrade() { + a.set_show_event_popup(true); + a.set_event_detail_time(to_s(&event.formatted_time())); + a.set_event_detail_level(to_s(&event.level)); + a.set_event_detail_message(to_s(&event.message)); + a.set_event_detail_source(to_s(&event.event_type)); + } + } + } + }); + + let app_handle = app.as_weak(); + app.on_close_event_details(move || { + if let Some(a) = app_handle.upgrade() { + a.set_show_event_popup(false); + } + }); + + // Create RAID dialog + let app_handle = app.as_weak(); + app.on_open_create_raid_dialog(move || { + if let Some(a) = app_handle.upgrade() { + a.set_show_create_raid_dialog(true); + a.set_create_raid_name(to_s("")); + a.set_create_raid_status(to_s("")); + a.set_create_raid_disk1(to_s("")); + a.set_create_raid_disk2(to_s("")); + a.set_create_raid_disk3(to_s("")); + a.set_create_raid_disk4(to_s("")); + } + }); + + let app_handle = app.as_weak(); + app.on_close_create_raid_dialog(move || { + if let Some(a) = app_handle.upgrade() { + a.set_show_create_raid_dialog(false); + } + }); + + let state = app_state.clone(); + let app_handle = app.as_weak(); + app.on_confirm_create_raid(move || { + let state = state.clone(); + let app_handle = app_handle.clone(); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + if let Ok(s) = state.lock() { + let _ = s.commit_config().await; + let _ = s.refresh().await; + if let Some(a) = app_handle.upgrade() { + a.set_show_create_raid_dialog(false); + update_ui_from_state(&a, &s); + } + } + }); + }); + + // Delete RAID dialog + let app_handle = app.as_weak(); + app.on_open_delete_raid_dialog(move || { + if let Some(a) = app_handle.upgrade() { + a.set_show_delete_raid_dialog(true); + a.set_delete_raid_name(to_s("RAID-1")); + a.set_delete_raid_status(to_s("")); + } + }); + + let app_handle = app.as_weak(); + app.on_close_delete_raid_dialog(move || { + if let Some(a) = app_handle.upgrade() { + a.set_show_delete_raid_dialog(false); + } + }); + + let state = app_state.clone(); + let app_handle = app.as_weak(); + app.on_confirm_delete_raid(move || { + let state = state.clone(); + let app_handle = app_handle.clone(); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + if let Ok(s) = state.lock() { + let _ = s.refresh().await; + if let Some(a) = app_handle.upgrade() { + a.set_show_delete_raid_dialog(false); + update_ui_from_state(&a, &s); + } + } + }); + }); + + // Rebuild RAID + let state = app_state.clone(); + app.on_rebuild_raid(move || { + let state = state.clone(); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + if let Ok(s) = state.lock() { + let _ = s.rebuild_raid(1).await; + } + }); + }); + + // Commit config + let state = app_state.clone(); + app.on_commit_config(move || { + let state = state.clone(); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + if let Ok(s) = state.lock() { + let _ = s.commit_config().await; + } + }); + }); + + // Disk operation dialog + let app_handle = app.as_weak(); + app.on_open_disk_operation_dialog(move || { + if let Some(a) = app_handle.upgrade() { + a.set_show_disk_dialog(true); + a.set_selected_disk(1); + a.set_disk_operation_status(to_s("")); + } + }); + + let app_handle = app.as_weak(); + app.on_close_disk_operation_dialog(move || { + if let Some(a) = app_handle.upgrade() { + a.set_show_disk_dialog(false); + } + }); + + let state = app_state.clone(); + let app_handle = app.as_weak(); + app.on_confirm_disk_operation(move || { + let state = state.clone(); + let app_handle = app_handle.clone(); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + if let Ok(s) = state.lock() { + let _ = s.refresh().await; + if let Some(a) = app_handle.upgrade() { + a.set_show_disk_dialog(false); + update_ui_from_state(&a, &s); + } + } + }); + }); + + app.run()?; + Ok(()) +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..eb08102 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,145 @@ +//! Data models for RAIDGuard X + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Controller { + pub id: i32, + pub ip: String, + pub hostname: String, + pub serial_number: String, + pub model: String, + pub firmware_version: String, + pub status: String, + pub enclosures: i32, + pub total_disks: i32, +} + +impl Controller { + pub fn from_json(value: &serde_json::Value) -> Option { + Some(Self { + id: value["id"].as_i64()? as i32, + ip: value["ip"].as_str()?.to_string(), + hostname: value["hostname"].as_str()?.to_string(), + serial_number: value["serial_number"].as_str()?.to_string(), + model: value["model"].as_str()?.to_string(), + firmware_version: value["firmware_version"].as_str()?.to_string(), + status: value["status"].as_str()?.to_string(), + enclosures: value["enclosures"].as_i64().unwrap_or(0) as i32, + total_disks: value["total_disks"].as_i64().unwrap_or(0) as i32, + }) + } + + pub fn is_online(&self) -> bool { + self.status == "online" + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RaidArray { + pub id: i32, + pub controller_id: i32, + pub name: String, + pub raid_level: String, + pub status: String, + pub total_capacity_tb: f64, + pub used_capacity_tb: f64, + pub free_capacity_tb: f64, + pub disk_count: i32, + pub rebuild_progress: Option, +} + +impl RaidArray { + pub fn from_json(value: &serde_json::Value) -> Option { + Some(Self { + id: value["id"].as_i64()? as i32, + controller_id: value["controller_id"].as_i64().unwrap_or(0) as i32, + name: value["name"].as_str()?.to_string(), + raid_level: value["raid_level"].as_str()?.to_string(), + status: value["status"].as_str()?.to_string(), + total_capacity_tb: value["total_capacity_tb"].as_f64().unwrap_or(0.0), + used_capacity_tb: value["used_capacity_tb"].as_f64().unwrap_or(0.0), + free_capacity_tb: value["free_capacity_tb"].as_f64().unwrap_or(0.0), + disk_count: value["disk_count"].as_i64().unwrap_or(0) as i32, + rebuild_progress: value["rebuild_progress"].as_i64().map(|v| v as i32), + }) + } + + pub fn usage_percent(&self) -> f64 { + if self.total_capacity_tb > 0.0 { + (self.used_capacity_tb / self.total_capacity_tb) * 100.0 + } else { + 0.0 + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Disk { + pub slot: i32, + pub enclosure: i32, + pub serial_number: String, + pub model: String, + pub vendor: String, + pub capacity_tb: f64, + pub status: String, + pub disk_type: String, + pub temperature: i32, + pub array_id: Option, + pub controller_id: i32, +} + +impl Disk { + pub fn from_json(value: &serde_json::Value) -> Option { + Some(Self { + slot: value["slot"].as_i64().unwrap_or(0) as i32, + enclosure: value["enclosure"].as_i64().unwrap_or(0) as i32, + serial_number: value["serial_number"].as_str().unwrap_or("").to_string(), + model: value["model"].as_str().unwrap_or("").to_string(), + vendor: value["vendor"].as_str().unwrap_or("").to_string(), + capacity_tb: value["capacity_tb"].as_f64().unwrap_or(0.0), + status: value["status"].as_str().unwrap_or("").to_string(), + disk_type: value["disk_type"].as_str().unwrap_or("").to_string(), + temperature: value["temperature"].as_i64().unwrap_or(0) as i32, + array_id: value["array_id"].as_i64().map(|v| v as i32), + controller_id: value["controller_id"].as_i64().unwrap_or(0) as i32, + }) + } + + pub fn is_online(&self) -> bool { + self.status == "online" + } + + pub fn is_failed(&self) -> bool { + self.status == "failed" + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Event { + pub id: i32, + pub controller_id: i32, + pub timestamp: i64, + pub level: String, + pub event_type: String, + pub message: String, +} + +impl Event { + pub fn from_json(value: &serde_json::Value) -> Option { + Some(Self { + id: value["id"].as_i64().unwrap_or(0) as i32, + controller_id: value["controller_id"].as_i64().unwrap_or(0) as i32, + timestamp: value["timestamp"].as_i64().unwrap_or(0), + level: value["level"].as_str().unwrap_or("info").to_string(), + event_type: value["event_type"].as_str().unwrap_or("").to_string(), + message: value["message"].as_str().unwrap_or("").to_string(), + }) + } + + pub fn formatted_time(&self) -> String { + use chrono::{DateTime, Utc}; + let dt = DateTime::from_timestamp(self.timestamp, 0).unwrap_or_else(|| Utc::now()); + dt.format("%Y-%m-%d %H:%M:%S").to_string() + } +} diff --git a/src/protocol/client.rs b/src/protocol/client.rs new file mode 100644 index 0000000..498b8d5 --- /dev/null +++ b/src/protocol/client.rs @@ -0,0 +1,275 @@ +//! TCP Client for communicating with GUI Server + +use tokio::net::TcpStream; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::sync::Mutex; +use anyhow::Result; +use std::sync::Arc; +use serde::{Deserialize, Serialize}; + +use crate::protocol::{Request, Response}; +use crate::app::Pagination; + +#[derive(Debug, Deserialize)] +pub struct EventsResponse { + pub events: Vec, + pub pagination: Pagination, +} + +#[derive(Clone)] +pub struct ServerClient { + stream: Arc>>, +} + +impl ServerClient { + pub async fn connect(addr: &str) -> Result { + tracing::info!("Connecting to server at {}...", addr); + let stream = TcpStream::connect(addr).await?; + stream.set_nodelay(true)?; + tracing::info!("Connected to server"); + + Ok(Self { + stream: Arc::new(Mutex::new(Some(stream))), + }) + } + + pub async fn send_request(&self, request: &Request) -> Result { + let mut stream_guard = self.stream.lock().await; + let stream = stream_guard.as_mut().ok_or_else(|| anyhow::anyhow!("Not connected"))?; + + // Serialize and send request + let json = serde_json::to_string(request)?; + let mut send_buf = json.into_bytes(); + send_buf.push(b'\n'); + + stream.write_all(&send_buf).await?; + + // Read response (read until newline) + let mut response_buf = Vec::new(); + let mut byte = [0u8; 1]; + loop { + let n = stream.read(&mut byte).await?; + if n == 0 { + break; + } + if byte[0] == b'\n' { + break; + } + response_buf.push(byte[0]); + } + let response_line = String::from_utf8(response_buf)?; + + let response: Response = serde_json::from_str(&response_line.trim())?; + Ok(response) + } + + pub async fn get_controllers(&self) -> Result> { + let request = Request::new("get_controllers"); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + let data = response.data.ok_or_else(|| anyhow::anyhow!("No data in response"))?; + let controllers = data["controllers"] + .as_array() + .cloned() + .unwrap_or_default(); + + Ok(controllers) + } + + pub async fn get_raids(&self, controller_id: Option) -> Result> { + let params = match controller_id { + Some(id) => serde_json::json!({ "controller_id": id }), + None => serde_json::json!({}), + }; + let request = Request::with_params("get_raids", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + let data = response.data.ok_or_else(|| anyhow::anyhow!("No data in response"))?; + let raids = data["raids"] + .as_array() + .cloned() + .unwrap_or_default(); + + Ok(raids) + } + + pub async fn get_disks(&self, controller_id: Option) -> Result> { + let params = match controller_id { + Some(id) => serde_json::json!({ "controller_id": id }), + None => serde_json::json!({}), + }; + let request = Request::with_params("get_disks", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + let data = response.data.ok_or_else(|| anyhow::anyhow!("No data in response"))?; + let disks = data["disks"] + .as_array() + .cloned() + .unwrap_or_default(); + + Ok(disks) + } + + pub async fn get_events(&self, controller_id: Option, limit: Option) -> Result> { + let mut params = serde_json::json!({}); + if let Some(id) = controller_id { + params["controller_id"] = serde_json::json!(id); + } + if let Some(l) = limit { + params["limit"] = serde_json::json!(l); + } + + let request = Request::with_params("get_events", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + let data = response.data.ok_or_else(|| anyhow::anyhow!("No data in response"))?; + let events = data["events"] + .as_array() + .cloned() + .unwrap_or_default(); + + Ok(events) + } + + pub async fn get_events_paginated(&self, page: i32, page_size: i32, level: Option) -> Result { + let mut params = serde_json::json!({ + "page": page, + "page_size": page_size + }); + if let Some(level) = level { + params["level"] = serde_json::json!(level); + } + + let request = Request::with_params("get_events", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + let data = response.data.ok_or_else(|| anyhow::anyhow!("No data in response"))?; + let events_response: EventsResponse = serde_json::from_value(data)?; + + Ok(events_response) + } + + pub async fn ping(&self) -> Result { + let request = Request::new("ping"); + let response = self.send_request(&request).await?; + Ok(response.is_success()) + } + + pub fn is_connected(&self) -> bool { + true + } + + // RAID Operations + pub async fn create_raid(&self, name: &str, level: u32, disks: Vec) -> Result { + let params = serde_json::json!({ + "name": name, + "level": level, + "disks": disks + }); + let request = Request::with_params("create_raid", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + response.data.ok_or_else(|| anyhow::anyhow!("No data in response")) + } + + pub async fn delete_raid(&self, raid_id: u32) -> Result { + let params = serde_json::json!({ "raid_id": raid_id }); + let request = Request::with_params("delete_raid", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + response.data.ok_or_else(|| anyhow::anyhow!("No data in response")) + } + + pub async fn rebuild_raid(&self, raid_id: u32) -> Result { + let params = serde_json::json!({ "raid_id": raid_id }); + let request = Request::with_params("rebuild_raid", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + response.data.ok_or_else(|| anyhow::anyhow!("No data in response")) + } + + pub async fn initialize_disk(&self, disk_id: u32) -> Result { + let params = serde_json::json!({ "disk_id": disk_id }); + let request = Request::with_params("initialize_disk", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + response.data.ok_or_else(|| anyhow::anyhow!("No data in response")) + } + + pub async fn locate_disk(&self, disk_id: u32) -> Result { + let params = serde_json::json!({ "disk_id": disk_id }); + let request = Request::with_params("locate_disk", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + response.data.ok_or_else(|| anyhow::anyhow!("No data in response")) + } + + pub async fn remove_disk(&self, disk_id: u32, force: bool) -> Result { + let params = serde_json::json!({ "disk_id": disk_id, "force": force }); + let request = Request::with_params("remove_disk", params); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + response.data.ok_or_else(|| anyhow::anyhow!("No data in response")) + } + + pub async fn commit_config(&self) -> Result { + let request = Request::new("commit_config"); + let response = self.send_request(&request).await?; + + if !response.is_success() { + return Err(anyhow::anyhow!("Error: {:?}", response.error)); + } + + response.data.ok_or_else(|| anyhow::anyhow!("No data in response")) + } +} + +impl Drop for ServerClient { + fn drop(&mut self) { + tracing::info!("ServerClient dropped"); + } +} diff --git a/src/protocol/messages.rs b/src/protocol/messages.rs new file mode 100644 index 0000000..ecf5845 --- /dev/null +++ b/src/protocol/messages.rs @@ -0,0 +1,69 @@ +//! Protocol messages - shared with server + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Request { + pub id: String, + #[serde(rename = "type")] + pub msg_type: String, + pub action: String, + #[serde(default)] + pub params: serde_json::Value, +} + +impl Request { + pub fn new(action: impl Into) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + msg_type: "request".to_string(), + action: action.into(), + params: serde_json::json!({}), + } + } + + pub fn with_params(action: impl Into, params: serde_json::Value) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + msg_type: "request".to_string(), + action: action.into(), + params, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Response { + pub id: String, + #[serde(rename = "type")] + pub msg_type: String, + pub status: String, + #[serde(default)] + pub data: Option, + #[serde(default)] + pub error: Option, +} + +impl Response { + pub fn is_success(&self) -> bool { + self.status == "success" + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Push { + #[serde(rename = "type")] + pub msg_type: String, + pub event: String, + #[serde(default)] + pub data: Option, +} + +pub mod actions { + pub const GET_CONTROLLERS: &str = "get_controllers"; + pub const GET_CONTROLLER_INFO: &str = "get_controller_info"; + pub const GET_RAIDS: &str = "get_raids"; + pub const GET_DISKS: &str = "get_disks"; + pub const GET_EVENTS: &str = "get_events"; + pub const PING: &str = "ping"; +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs new file mode 100644 index 0000000..176233f --- /dev/null +++ b/src/protocol/mod.rs @@ -0,0 +1,7 @@ +//! Client protocol module + +pub mod client; +pub mod messages; + +pub use client::*; +pub use messages::*; diff --git a/ui/main_window.slint b/ui/main_window.slint new file mode 100644 index 0000000..845290d --- /dev/null +++ b/ui/main_window.slint @@ -0,0 +1,515 @@ +// RAIDGuard X - Complete GUI Recreation +// Mimics Java Swing frmMain.java appearance + +export component AppWindow inherits Window { + title: "RAIDGuard X - DTR RAID Admin"; + width: 1280px; + height: 800px; + background: #f0f0f0; + + callback connect_server(); + callback refresh_data(); + callback menu_about(); + callback menu_exit(); + callback menu_add_controller(); + + property connected: false; + property is_loading: false; + property status_text: "Disconnected"; + property current_tab: 0; + + property controller_count: 0; + property raid_count: 0; + property disk_count: 0; + property auto_refresh: true; + + property selected_controller_index: 0; + property ctrl1_name: "RAID Controller"; + property ctrl1_ip: "192.168.1.100"; + property ctrl1_status: "Online"; + property ctrl1_model: "Accusys RAID 9000"; + property ctrl1_firmware: "v3.8.0"; + property ctrl1_sn: "ACC123456789"; + property ctrl1_vendor: "Accusys"; + property ctrl2_name: "-"; + property ctrl2_ip: "-"; + property ctrl2_status: "-"; + property ctrl2_model: "-"; + property ctrl2_firmware: "-"; + property ctrl2_sn: "-"; + property ctrl2_vendor: "-"; + + property raid1_name: "RAID-01"; + property raid1_level: "RAID 5"; + property raid1_status: "Normal"; + property raid1_capacity: "2.0 TB"; + property raid1_usage: "45%"; + property raid1_usage_pct: 45.0; + property raid2_name: "RAID-02"; + property raid2_level: "RAID 6"; + property raid2_status: "Normal"; + property raid2_capacity: "4.0 TB"; + property raid2_usage: "30%"; + property raid2_usage_pct: 30.0; + + property disk1_loc: "Enclosure 0 Slot 0"; + property disk1_model: "ST3000VX000"; + property disk1_status: "Online"; + property disk1_capacity: "3.0 TB"; + property disk1_vendor: "Seagate"; + property disk2_loc: "Enclosure 0 Slot 1"; + property disk2_model: "ST3000VX000"; + property disk2_status: "Online"; + property disk2_capacity: "3.0 TB"; + property disk2_vendor: "Seagate"; + property disk3_loc: "Enclosure 0 Slot 2"; + property disk3_model: "ST3000VX000"; + property disk3_status: "Online"; + property disk3_capacity: "3.0 TB"; + property disk3_vendor: "Seagate"; + property disk4_loc: "Enclosure 0 Slot 3"; + property disk4_model: "ST3000VX000"; + property disk4_status: "Online"; + property disk4_capacity: "3.0 TB"; + property disk4_vendor: "Seagate"; + + property evtime1: "2026/03/29-10:30:00"; + property evlevel1: "Info"; + property evmsg1: "Controller started successfully"; + property evsource1: "System"; + property evtime2: "2026/03/29-10:25:00"; + property evlevel2: "Info"; + property evmsg2: "Disk online"; + property evsource2: "Disk"; + property evtime3: "2026/03/29-10:20:00"; + property evlevel3: "Warning"; + property evmsg3: "Temperature warning"; + property evsource3: "Enclosure"; + property evtime4: "2026/03/29-10:15:00"; + property evlevel4: "Info"; + property evmsg4: "RAID-01 status: Normal"; + property evsource4: "RAID"; + property evtime5: "2026/03/29-10:10:00"; + property evlevel5: "Info"; + property evmsg5: "Power supply online"; + property evsource5: "Power"; + + VerticalLayout { + spacing: 0; + + // Menu Bar + Rectangle { + height: 24px; + background: #f0f0f0; + HorizontalLayout { + + spacing: 16px; + Text { text: "File"; font-weight: 500; color: #000000; font_size: 13px; } + Text { text: "Controller"; font-weight: 500; color: #000000; font_size: 13px; } + Text { text: "Tools"; font-weight: 500; color: #000000; font_size: 13px; } + Text { text: "Help"; font-weight: 500; color: #000000; font_size: 13px; } + } + } + + // Toolbar + Rectangle { + height: 40px; + background: #e8e8e8; + HorizontalLayout { + + spacing: 8px; + Rectangle { + width: 80px; + height: 32px; + background: #ffffff; + border-radius: 3px; + Text { text: "Add"; font_size: 12px; color: #000000; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { + width: 80px; + height: 32px; + background: #ffffff; + border-radius: 3px; + Text { text: "Remove"; font_size: 12px; color: #000000; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { width: 10px; } + Rectangle { + width: 90px; + height: 32px; + background: #4a90d9; + border-radius: 3px; + Text { text: "Create"; font_size: 12px; color: white; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { + width: 80px; + height: 32px; + background: #d94a4a; + border-radius: 3px; + Text { text: "Delete"; font_size: 12px; color: white; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { width: 10px; } + Rectangle { + width: 80px; + height: 32px; + background: #ffffff; + border-radius: 3px; + Text { text: "Email"; font_size: 12px; color: #000000; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { + width: 80px; + height: 32px; + background: #ffffff; + border-radius: 3px; + Text { text: "Settings"; font_size: 12px; color: #000000; horizontal-alignment: center; vertical-alignment: center; } + } + } + } + + // Main Content + HorizontalLayout { + spacing: 0; + height: 1px; + + // Left Panel - Host List + Rectangle { + width: 280px; + background: #ffffff; + VerticalLayout { + padding: 0; + spacing: 0; + Rectangle { + height: 30px; + background: #d0d0d0; + Text { text: "Host List"; font-weight: 600; font_size: 13px; color: #000000; vertical-alignment: center; } + } + Rectangle { + height: 24px; + background: #e0e0e0; + HorizontalLayout { + + spacing: 4px; + Text { text: "Host"; font_size: 11px; font-weight: 600; width: 70px; } + Text { text: "IP"; font_size: 11px; font-weight: 600; width: 90px; } + Text { text: "Status"; font_size: 11px; font-weight: 600; width: 60px; } + } + } + Rectangle { + height: 28px; + background: #c0c0c0; + HorizontalLayout { + + spacing: 4px; + Text { text: ctrl1_name; font_size: 11px; width: 70px; } + Text { text: ctrl1_ip; font_size: 11px; width: 90px; } + Text { text: ctrl1_status; font_size: 11px; color: #008000; width: 60px; } + } + } + Rectangle { + height: 28px; + background: #ffffff; + HorizontalLayout { + + spacing: 4px; + Text { text: ctrl2_name; font_size: 11px; width: 70px; } + Text { text: ctrl2_ip; font_size: 11px; width: 90px; } + Text { text: ctrl2_status; font_size: 11px; width: 60px; } + } + } + Rectangle { } + } + } + + // Right Panel - Tabs + VerticalLayout { + spacing: 0; + + // Tab Header + Rectangle { + height: 36px; + background: #d0d0d0; + HorizontalLayout { + spacing: 0; + Rectangle { + width: 120px; + background: current_tab == 0 ? #ffffff : #d0d0d0; + Text { text: "Controller"; font_size: 13px; font-weight: current_tab == 0 ? 600 : 400; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { + width: 100px; + background: current_tab == 1 ? #ffffff : #d0d0d0; + Text { text: "RAID"; font_size: 13px; font-weight: current_tab == 1 ? 600 : 400; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { + width: 100px; + background: current_tab == 2 ? #ffffff : #d0d0d0; + Text { text: "Drives"; font_size: 13px; font-weight: current_tab == 2 ? 600 : 400; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { + width: 100px; + background: current_tab == 3 ? #ffffff : #d0d0d0; + Text { text: "Event"; font_size: 13px; font-weight: current_tab == 3 ? 600 : 400; horizontal-alignment: center; vertical-alignment: center; } + } + } + } + + // Tab Content + Rectangle { + background: #ffffff; + height: 1px; + + // Controller Tab + if current_tab == 0: VerticalLayout { + + spacing: 12px; + Rectangle { + background: #f0f0f0; + + VerticalLayout { + spacing: 8px; + Text { text: "Controller Information"; font-weight: 600; font_size: 14px; } + HorizontalLayout { + spacing: 12px; + VerticalLayout { spacing: 6px; + Text { text: "Controller Name:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + Text { text: "Model Name:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + Text { text: "Serial Number:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + Text { text: "Firmware Version:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + Text { text: "BIOS Version:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + Text { text: "Status:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + } + VerticalLayout { spacing: 6px; + Text { text: ctrl1_name; font_size: 12px; } + Text { text: ctrl1_model; font_size: 12px; } + Text { text: ctrl1_sn; font_size: 12px; } + Text { text: ctrl1_firmware; font_size: 12px; } + Text { text: "1.2.3.4"; font_size: 12px; } + Text { text: ctrl1_status; font_size: 12px; color: #008000; } + } + } + } + } + } + + // RAID Tab + if current_tab == 1: VerticalLayout { + + spacing: 12px; + Rectangle { + background: #f0f0f0; + + VerticalLayout { + spacing: 8px; + HorizontalLayout { + Text { text: raid1_name; font-weight: 600; font_size: 14px; } + Text { text: raid1_status; font_size: 12px; color: #008000; } + } + HorizontalLayout { + spacing: 12px; + VerticalLayout { spacing: 6px; + Text { text: "RAID Level:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + Text { text: "Capacity:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + Text { text: "Usage:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + } + VerticalLayout { spacing: 6px; + Text { text: raid1_level; font_size: 12px; } + Text { text: raid1_capacity; font_size: 12px; } + Text { text: raid1_usage; font_size: 12px; } + } + } + } + } + Rectangle { + background: #f0f0f0; + + VerticalLayout { + spacing: 8px; + HorizontalLayout { + Text { text: raid2_name; font_weight: 600; font_size: 14px; } + Text { text: raid2_status; font_size: 12px; color: #008000; } + } + HorizontalLayout { + spacing: 12px; + VerticalLayout { spacing: 6px; + Text { text: "RAID Level:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + Text { text: "Capacity:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + Text { text: "Usage:"; font_size: 12px; font-weight: 600; horizontal-alignment: right; } + } + VerticalLayout { spacing: 6px; + Text { text: raid2_level; font_size: 12px; } + Text { text: raid2_capacity; font_size: 12px; } + Text { text: raid2_usage; font_size: 12px; } + } + } + } + } + } + + // Drives Tab + if current_tab == 2: VerticalLayout { + + spacing: 4px; + Rectangle { + height: 28px; + background: #e0e0e0; + HorizontalLayout { + + spacing: 16px; + Text { text: "Location"; font_size: 11px; font-weight: 600; width: 140px; } + Text { text: "Vendor"; font_size: 11px; font-weight: 600; width: 100px; } + Text { text: "Model"; font_size: 11px; font-weight: 600; width: 160px; } + Text { text: "Capacity"; font_size: 11px; font-weight: 600; width: 80px; } + Text { text: "Status"; font_size: 11px; font-weight: 600; width: 80px; } + } + } + Rectangle { + height: 26px; + background: #f0f0f0; + HorizontalLayout { + + spacing: 16px; + Text { text: disk1_loc; font_size: 11px; width: 140px; } + Text { text: disk1_vendor; font_size: 11px; width: 100px; } + Text { text: disk1_model; font_size: 11px; width: 160px; } + Text { text: disk1_capacity; font_size: 11px; width: 80px; } + Text { text: disk1_status; font_size: 11px; color: #008000; width: 80px; } + } + } + Rectangle { + height: 26px; + background: #ffffff; + HorizontalLayout { + + spacing: 16px; + Text { text: disk2_loc; font_size: 11px; width: 140px; } + Text { text: disk2_vendor; font_size: 11px; width: 100px; } + Text { text: disk2_model; font_size: 11px; width: 160px; } + Text { text: disk2_capacity; font_size: 11px; width: 80px; } + Text { text: disk2_status; font_size: 11px; color: #008000; width: 80px; } + } + } + Rectangle { + height: 26px; + background: #f0f0f0; + HorizontalLayout { + + spacing: 16px; + Text { text: disk3_loc; font_size: 11px; width: 140px; } + Text { text: disk3_vendor; font_size: 11px; width: 100px; } + Text { text: disk3_model; font_size: 11px; width: 160px; } + Text { text: disk3_capacity; font_size: 11px; width: 80px; } + Text { text: disk3_status; font_size: 11px; color: #008000; width: 80px; } + } + } + Rectangle { + height: 26px; + background: #ffffff; + HorizontalLayout { + + spacing: 16px; + Text { text: disk4_loc; font_size: 11px; width: 140px; } + Text { text: disk4_vendor; font_size: 11px; width: 100px; } + Text { text: disk4_model; font_size: 11px; width: 160px; } + Text { text: disk4_capacity; font_size: 11px; width: 80px; } + Text { text: disk4_status; font_size: 11px; color: #008000; width: 80px; } + } + } + } + + // Event Tab + if current_tab == 3: VerticalLayout { + + spacing: 4px; + Rectangle { + height: 28px; + background: #e0e0e0; + HorizontalLayout { + + spacing: 8px; + Text { text: "Date/Time"; font_size: 11px; font-weight: 600; width: 140px; } + Text { text: "Level"; font_size: 11px; font-weight: 600; width: 60px; } + Text { text: "Source"; font_size: 11px; font-weight: 600; width: 80px; } + Text { text: "Message"; font_size: 11px; font-weight: 600; width: 400px; } + } + } + Rectangle { + height: 24px; + background: #f0f0f0; + HorizontalLayout { + + spacing: 8px; + Text { text: evtime1; font_size: 11px; width: 140px; } + Text { text: evlevel1; font_size: 11px; color: #008000; width: 60px; } + Text { text: evsource1; font_size: 11px; width: 80px; } + Text { text: evmsg1; font_size: 11px; width: 400px; } + } + } + Rectangle { + height: 24px; + background: #ffffff; + HorizontalLayout { + + spacing: 8px; + Text { text: evtime2; font_size: 11px; width: 140px; } + Text { text: evlevel2; font_size: 11px; color: #008000; width: 60px; } + Text { text: evsource2; font_size: 11px; width: 80px; } + Text { text: evmsg2; font_size: 11px; width: 400px; } + } + } + Rectangle { + height: 24px; + background: #f0f0f0; + HorizontalLayout { + + spacing: 8px; + Text { text: evtime3; font_size: 11px; width: 140px; } + Text { text: evlevel3; font_size: 11px; color: #ffa500; width: 60px; } + Text { text: evsource3; font_size: 11px; width: 80px; } + Text { text: evmsg3; font_size: 11px; width: 400px; } + } + } + Rectangle { + height: 24px; + background: #ffffff; + HorizontalLayout { + + spacing: 8px; + Text { text: evtime4; font_size: 11px; width: 140px; } + Text { text: evlevel4; font_size: 11px; color: #008000; width: 60px; } + Text { text: evsource4; font_size: 11px; width: 80px; } + Text { text: evmsg4; font_size: 11px; width: 400px; } + } + } + Rectangle { + height: 24px; + background: #f0f0f0; + HorizontalLayout { + + spacing: 8px; + Text { text: evtime5; font_size: 11px; width: 140px; } + Text { text: evlevel5; font_size: 11px; color: #008000; width: 60px; } + Text { text: evsource5; font_size: 11px; width: 80px; } + Text { text: evmsg5; font_size: 11px; width: 400px; } + } + } + } + } + } + } + + // Status Bar + Rectangle { + height: 26px; + background: #d0d0d0; + HorizontalLayout { + + spacing: 16px; + Text { text: "Controllers: " + controller_count; font_size: 11px; color: #000000; } + Text { text: "RAID Arrays: " + raid_count; font_size: 11px; color: #000000; } + Text { text: "Drives: " + disk_count; font_size: 11px; color: #000000; } + Rectangle { width: 300px; } + Text { text: "Auto Refresh: " + (auto_refresh ? "On" : "Off"); font_size: 11px; color: #000000; } + Text { text: status_text; font_size: 11px; color: connected ? #008000 : #ff0000; } + } + } + } +}