Complete Slint UI mimicking Java Swing frmMain
This commit is contained in:
31
Cargo.toml
Normal file
31
Cargo.toml
Normal file
@@ -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"
|
||||
3
build.rs
Normal file
3
build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
slint_build::compile("ui/main_window.slint").unwrap();
|
||||
}
|
||||
BIN
icons/ball-green.png
Normal file
BIN
icons/ball-green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 925 B |
BIN
icons/disk-1.png
Normal file
BIN
icons/disk-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
icons/func-settings.png
Normal file
BIN
icons/func-settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
272
src/app.rs
Normal file
272
src/app.rs
Normal file
@@ -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<RwLock<Option<ServerClient>>>,
|
||||
pub controllers: Arc<RwLock<Vec<Controller>>>,
|
||||
pub raids: Arc<RwLock<Vec<RaidArray>>>,
|
||||
pub disks: Arc<RwLock<Vec<Disk>>>,
|
||||
pub events: Arc<RwLock<Vec<Event>>>,
|
||||
pub selected_controller: Arc<RwLock<Option<i32>>>,
|
||||
pub selected_level: Arc<RwLock<String>>,
|
||||
pub connected: Arc<RwLock<bool>>,
|
||||
}
|
||||
|
||||
#[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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<Controller> {
|
||||
self.controllers.read().clone()
|
||||
}
|
||||
|
||||
pub fn get_raids(&self) -> Vec<RaidArray> {
|
||||
self.raids.read().clone()
|
||||
}
|
||||
|
||||
pub fn get_disks(&self) -> Vec<Disk> {
|
||||
self.disks.read().clone()
|
||||
}
|
||||
|
||||
pub fn get_events(&self) -> Vec<Event> {
|
||||
self.events.read().clone()
|
||||
}
|
||||
|
||||
pub async fn load_events_page(&self, page: i32, page_size: i32) -> Result<Option<Pagination>> {
|
||||
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<u32>) -> Result<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
@@ -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;
|
||||
467
src/main.rs
Normal file
467
src/main.rs
Normal file
@@ -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(())
|
||||
}
|
||||
145
src/models/mod.rs
Normal file
145
src/models/mod.rs
Normal file
@@ -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<Self> {
|
||||
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<i32>,
|
||||
}
|
||||
|
||||
impl RaidArray {
|
||||
pub fn from_json(value: &serde_json::Value) -> Option<Self> {
|
||||
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<i32>,
|
||||
pub controller_id: i32,
|
||||
}
|
||||
|
||||
impl Disk {
|
||||
pub fn from_json(value: &serde_json::Value) -> Option<Self> {
|
||||
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<Self> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
275
src/protocol/client.rs
Normal file
275
src/protocol/client.rs
Normal file
@@ -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<serde_json::Value>,
|
||||
pub pagination: Pagination,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerClient {
|
||||
stream: Arc<Mutex<Option<TcpStream>>>,
|
||||
}
|
||||
|
||||
impl ServerClient {
|
||||
pub async fn connect(addr: &str) -> Result<Self> {
|
||||
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<Response> {
|
||||
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<Vec<serde_json::Value>> {
|
||||
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<i32>) -> Result<Vec<serde_json::Value>> {
|
||||
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<i32>) -> Result<Vec<serde_json::Value>> {
|
||||
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<i32>, limit: Option<usize>) -> Result<Vec<serde_json::Value>> {
|
||||
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<String>) -> Result<EventsResponse> {
|
||||
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<bool> {
|
||||
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<u32>) -> Result<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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<serde_json::Value> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
69
src/protocol/messages.rs
Normal file
69
src/protocol/messages.rs
Normal file
@@ -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<String>) -> 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<String>, 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_json::Value>,
|
||||
#[serde(default)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
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<serde_json::Value>,
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
7
src/protocol/mod.rs
Normal file
7
src/protocol/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! Client protocol module
|
||||
|
||||
pub mod client;
|
||||
pub mod messages;
|
||||
|
||||
pub use client::*;
|
||||
pub use messages::*;
|
||||
515
ui/main_window.slint
Normal file
515
ui/main_window.slint
Normal file
@@ -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<bool> connected: false;
|
||||
property<bool> is_loading: false;
|
||||
property<string> status_text: "Disconnected";
|
||||
property<int> current_tab: 0;
|
||||
|
||||
property<int> controller_count: 0;
|
||||
property<int> raid_count: 0;
|
||||
property<int> disk_count: 0;
|
||||
property<bool> auto_refresh: true;
|
||||
|
||||
property<int> selected_controller_index: 0;
|
||||
property<string> ctrl1_name: "RAID Controller";
|
||||
property<string> ctrl1_ip: "192.168.1.100";
|
||||
property<string> ctrl1_status: "Online";
|
||||
property<string> ctrl1_model: "Accusys RAID 9000";
|
||||
property<string> ctrl1_firmware: "v3.8.0";
|
||||
property<string> ctrl1_sn: "ACC123456789";
|
||||
property<string> ctrl1_vendor: "Accusys";
|
||||
property<string> ctrl2_name: "-";
|
||||
property<string> ctrl2_ip: "-";
|
||||
property<string> ctrl2_status: "-";
|
||||
property<string> ctrl2_model: "-";
|
||||
property<string> ctrl2_firmware: "-";
|
||||
property<string> ctrl2_sn: "-";
|
||||
property<string> ctrl2_vendor: "-";
|
||||
|
||||
property<string> raid1_name: "RAID-01";
|
||||
property<string> raid1_level: "RAID 5";
|
||||
property<string> raid1_status: "Normal";
|
||||
property<string> raid1_capacity: "2.0 TB";
|
||||
property<string> raid1_usage: "45%";
|
||||
property<float> raid1_usage_pct: 45.0;
|
||||
property<string> raid2_name: "RAID-02";
|
||||
property<string> raid2_level: "RAID 6";
|
||||
property<string> raid2_status: "Normal";
|
||||
property<string> raid2_capacity: "4.0 TB";
|
||||
property<string> raid2_usage: "30%";
|
||||
property<float> raid2_usage_pct: 30.0;
|
||||
|
||||
property<string> disk1_loc: "Enclosure 0 Slot 0";
|
||||
property<string> disk1_model: "ST3000VX000";
|
||||
property<string> disk1_status: "Online";
|
||||
property<string> disk1_capacity: "3.0 TB";
|
||||
property<string> disk1_vendor: "Seagate";
|
||||
property<string> disk2_loc: "Enclosure 0 Slot 1";
|
||||
property<string> disk2_model: "ST3000VX000";
|
||||
property<string> disk2_status: "Online";
|
||||
property<string> disk2_capacity: "3.0 TB";
|
||||
property<string> disk2_vendor: "Seagate";
|
||||
property<string> disk3_loc: "Enclosure 0 Slot 2";
|
||||
property<string> disk3_model: "ST3000VX000";
|
||||
property<string> disk3_status: "Online";
|
||||
property<string> disk3_capacity: "3.0 TB";
|
||||
property<string> disk3_vendor: "Seagate";
|
||||
property<string> disk4_loc: "Enclosure 0 Slot 3";
|
||||
property<string> disk4_model: "ST3000VX000";
|
||||
property<string> disk4_status: "Online";
|
||||
property<string> disk4_capacity: "3.0 TB";
|
||||
property<string> disk4_vendor: "Seagate";
|
||||
|
||||
property<string> evtime1: "2026/03/29-10:30:00";
|
||||
property<string> evlevel1: "Info";
|
||||
property<string> evmsg1: "Controller started successfully";
|
||||
property<string> evsource1: "System";
|
||||
property<string> evtime2: "2026/03/29-10:25:00";
|
||||
property<string> evlevel2: "Info";
|
||||
property<string> evmsg2: "Disk online";
|
||||
property<string> evsource2: "Disk";
|
||||
property<string> evtime3: "2026/03/29-10:20:00";
|
||||
property<string> evlevel3: "Warning";
|
||||
property<string> evmsg3: "Temperature warning";
|
||||
property<string> evsource3: "Enclosure";
|
||||
property<string> evtime4: "2026/03/29-10:15:00";
|
||||
property<string> evlevel4: "Info";
|
||||
property<string> evmsg4: "RAID-01 status: Normal";
|
||||
property<string> evsource4: "RAID";
|
||||
property<string> evtime5: "2026/03/29-10:10:00";
|
||||
property<string> evlevel5: "Info";
|
||||
property<string> evmsg5: "Power supply online";
|
||||
property<string> 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user