From d604bb811931a812311440c3a2000c2f7ad7d559 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Tue, 22 Jul 2025 16:20:39 +0300 Subject: [PATCH] GUI Feature --- Cargo.lock | 17 +- Cargo.toml | 1 + src/gui/mod.rs | 4 +- src/gui/settings/cross.rs.bak | 75 ------ src/gui/settings/macos.rs.bak | 414 ----------------------------- src/gui/settings/mod.rs | 475 +++++++++++++++++++++++++++++----- 6 files changed, 429 insertions(+), 557 deletions(-) delete mode 100644 src/gui/settings/cross.rs.bak delete mode 100644 src/gui/settings/macos.rs.bak diff --git a/Cargo.lock b/Cargo.lock index 565d2bc..d396c5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1593,7 +1593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2697,6 +2697,7 @@ dependencies = [ "tokio-util", "tray-icon", "trust-dns-resolver", + "urlencoding", "winit", ] @@ -2790,7 +2791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.2", ] [[package]] @@ -4112,7 +4113,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4125,7 +4126,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -5059,6 +5060,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.2" @@ -5470,7 +5477,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6554452..8130608 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,4 +29,5 @@ eframe = "0.29" egui = "0.29" winit = "0.30" env_logger = "0.11" +urlencoding = "2.1" diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 996ea79..fb001b5 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -425,7 +425,7 @@ impl ApplicationHandler for Application { } } - tray_icon.set_tooltip(Some(&tooltip)); + let _ = tray_icon.set_tooltip(Some(&tooltip)); } } drop(settings); @@ -511,7 +511,7 @@ impl ApplicationHandler for Application { } } - tray_icon.set_tooltip(Some(&tooltip)); + let _ = tray_icon.set_tooltip(Some(&tooltip)); } // Restart auto sync if interval changed or settings changed diff --git a/src/gui/settings/cross.rs.bak b/src/gui/settings/cross.rs.bak deleted file mode 100644 index 32fbcd7..0000000 --- a/src/gui/settings/cross.rs.bak +++ /dev/null @@ -1,75 +0,0 @@ -use super::{load_settings, save_settings, KhmSettings}; -use eframe::egui; -use log::error; - -struct KhmSettingsWindow { - settings: KhmSettings, -} - -impl eframe::App for KhmSettingsWindow { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("KHM Settings"); - ui.separator(); - - ui.horizontal(|ui| { - ui.label("Host URL:"); - ui.text_edit_singleline(&mut self.settings.host); - }); - - ui.horizontal(|ui| { - ui.label("Flow Name:"); - ui.text_edit_singleline(&mut self.settings.flow); - }); - - ui.horizontal(|ui| { - ui.label("Known Hosts:"); - ui.text_edit_singleline(&mut self.settings.known_hosts); - }); - - ui.horizontal(|ui| { - ui.label("Basic Auth:"); - ui.text_edit_singleline(&mut self.settings.basic_auth); - }); - - ui.horizontal(|ui| { - ui.label("Auto sync interval (minutes):"); - ui.add(egui::DragValue::new(&mut self.settings.auto_sync_interval_minutes).range(5..=1440)); - }); - - ui.checkbox(&mut self.settings.in_place, "Update known_hosts file in-place after sync"); - - ui.separator(); - - ui.horizontal(|ui| { - if ui.button("Save").clicked() { - if let Err(e) = save_settings(&self.settings) { - error!("Failed to save KHM settings: {}", e); - } - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - - if ui.button("Cancel").clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - }); - }); - } -} - -pub fn run_settings_window() { - let settings = load_settings(); - - let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_title("KHM Settings") - .with_inner_size([450.0, 385.0]), - ..Default::default() - }; - - let _ = eframe::run_native( - "KHM Settings", - options, - Box::new(|_cc| Ok(Box::new(KhmSettingsWindow { settings }))), - ); -} diff --git a/src/gui/settings/macos.rs.bak b/src/gui/settings/macos.rs.bak deleted file mode 100644 index 6e3050f..0000000 --- a/src/gui/settings/macos.rs.bak +++ /dev/null @@ -1,414 +0,0 @@ -#![allow(unexpected_cfgs)] -use super::{load_settings, save_settings, KhmSettings}; -use cocoa::appkit::*; -use cocoa::base::{id, nil, NO, YES}; -use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString, NSDefaultRunLoopMode}; -use log::{debug, error, info}; -use objc::{msg_send, sel, sel_impl}; -use std::ffi::CStr; - -// NSTextFieldBezelStyle constants -#[allow(non_upper_case_globals)] -const NSTextFieldSquareBezel: u32 = 0; - -const WINDOW_WIDTH: f64 = 450.0; -const WINDOW_HEIGHT: f64 = 385.0; -const MARGIN: f64 = 20.0; -const FIELD_HEIGHT: f64 = 24.0; -const BUTTON_HEIGHT: f64 = 32.0; - -// NSControl state constants -const NS_CONTROL_STATE_VALUE_OFF: i32 = 0; -const NS_CONTROL_STATE_VALUE_ON: i32 = 1; - -// NSButton type constants -const NS_SWITCH_BUTTON: u32 = 3; - -struct MacOSKhmSettingsWindow { - window: id, - host_field: id, - flow_field: id, - known_hosts_field: id, - basic_auth_field: id, - auto_sync_field: id, - in_place_checkbox: id, -} - -impl MacOSKhmSettingsWindow { - fn new() -> Self { - info!("Creating macOS KHM settings window"); - unsafe { - let settings = load_settings(); - info!("KHM Settings loaded: host={}, flow={}", settings.host, settings.flow); - - // Create window - let window: id = msg_send![NSWindow::alloc(nil), - initWithContentRect: NSRect::new( - NSPoint::new(100.0, 100.0), - NSSize::new(WINDOW_WIDTH, WINDOW_HEIGHT), - ) - styleMask: NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask - backing: NSBackingStoreType::NSBackingStoreBuffered - defer: NO - ]; - info!("Window allocated and initialized"); - - let _: () = msg_send![window, setTitle: NSString::alloc(nil).init_str("KHM Settings")]; - let _: () = msg_send![window, center]; - let _: () = msg_send![window, setReleasedWhenClosed: NO]; - - let content_view: id = msg_send![window, contentView]; - - let mut current_y = WINDOW_HEIGHT - MARGIN - 30.0; - - // Host label and field - let host_label: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN, current_y), - NSSize::new(100.0, 20.0), - ) - ]; - let _: () = msg_send![host_label, setStringValue: NSString::alloc(nil).init_str("Host URL:")]; - let _: () = msg_send![host_label, setBezeled: NO]; - let _: () = msg_send![host_label, setDrawsBackground: NO]; - let _: () = msg_send![host_label, setEditable: NO]; - let _: () = msg_send![host_label, setSelectable: NO]; - let _: () = msg_send![content_view, addSubview: host_label]; - - let host_field: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN + 110.0, current_y), - NSSize::new(310.0, FIELD_HEIGHT), - ) - ]; - let _: () = msg_send![host_field, setStringValue: NSString::alloc(nil).init_str(&settings.host)]; - let _: () = msg_send![host_field, setEditable: YES]; - let _: () = msg_send![host_field, setSelectable: YES]; - let _: () = msg_send![host_field, setBezeled: YES]; - let _: () = msg_send![host_field, setBezelStyle: NSTextFieldSquareBezel]; - let _: () = msg_send![content_view, addSubview: host_field]; - - current_y -= 35.0; - - // Flow label and field - let flow_label: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN, current_y), - NSSize::new(100.0, 20.0), - ) - ]; - let _: () = msg_send![flow_label, setStringValue: NSString::alloc(nil).init_str("Flow Name:")]; - let _: () = msg_send![flow_label, setBezeled: NO]; - let _: () = msg_send![flow_label, setDrawsBackground: NO]; - let _: () = msg_send![flow_label, setEditable: NO]; - let _: () = msg_send![flow_label, setSelectable: NO]; - let _: () = msg_send![content_view, addSubview: flow_label]; - - let flow_field: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN + 110.0, current_y), - NSSize::new(310.0, FIELD_HEIGHT), - ) - ]; - let _: () = msg_send![flow_field, setStringValue: NSString::alloc(nil).init_str(&settings.flow)]; - let _: () = msg_send![flow_field, setEditable: YES]; - let _: () = msg_send![flow_field, setSelectable: YES]; - let _: () = msg_send![flow_field, setBezeled: YES]; - let _: () = msg_send![flow_field, setBezelStyle: NSTextFieldSquareBezel]; - let _: () = msg_send![content_view, addSubview: flow_field]; - - current_y -= 35.0; - - // Known hosts label and field - let known_hosts_label: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN, current_y), - NSSize::new(100.0, 20.0), - ) - ]; - let _: () = msg_send![known_hosts_label, setStringValue: NSString::alloc(nil).init_str("Known Hosts:")]; - let _: () = msg_send![known_hosts_label, setBezeled: NO]; - let _: () = msg_send![known_hosts_label, setDrawsBackground: NO]; - let _: () = msg_send![known_hosts_label, setEditable: NO]; - let _: () = msg_send![known_hosts_label, setSelectable: NO]; - let _: () = msg_send![content_view, addSubview: known_hosts_label]; - - let known_hosts_field: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN + 110.0, current_y), - NSSize::new(310.0, FIELD_HEIGHT), - ) - ]; - let _: () = msg_send![known_hosts_field, setStringValue: NSString::alloc(nil).init_str(&settings.known_hosts)]; - let _: () = msg_send![known_hosts_field, setEditable: YES]; - let _: () = msg_send![known_hosts_field, setSelectable: YES]; - let _: () = msg_send![known_hosts_field, setBezeled: YES]; - let _: () = msg_send![known_hosts_field, setBezelStyle: NSTextFieldSquareBezel]; - let _: () = msg_send![content_view, addSubview: known_hosts_field]; - - current_y -= 35.0; - - // Basic auth label and field - let basic_auth_label: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN, current_y), - NSSize::new(100.0, 20.0), - ) - ]; - let _: () = msg_send![basic_auth_label, setStringValue: NSString::alloc(nil).init_str("Basic Auth:")]; - let _: () = msg_send![basic_auth_label, setBezeled: NO]; - let _: () = msg_send![basic_auth_label, setDrawsBackground: NO]; - let _: () = msg_send![basic_auth_label, setEditable: NO]; - let _: () = msg_send![basic_auth_label, setSelectable: NO]; - let _: () = msg_send![content_view, addSubview: basic_auth_label]; - - let basic_auth_field: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN + 110.0, current_y), - NSSize::new(310.0, FIELD_HEIGHT), - ) - ]; - let _: () = msg_send![basic_auth_field, setStringValue: NSString::alloc(nil).init_str(&settings.basic_auth)]; - let _: () = msg_send![basic_auth_field, setEditable: YES]; - let _: () = msg_send![basic_auth_field, setSelectable: YES]; - let _: () = msg_send![basic_auth_field, setBezeled: YES]; - let _: () = msg_send![basic_auth_field, setBezelStyle: NSTextFieldSquareBezel]; - let _: () = msg_send![content_view, addSubview: basic_auth_field]; - - current_y -= 35.0; - - // Auto sync interval label and field - let auto_sync_label: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN, current_y), - NSSize::new(100.0, 20.0), - ) - ]; - let _: () = msg_send![auto_sync_label, setStringValue: NSString::alloc(nil).init_str("Auto sync (min):")]; - let _: () = msg_send![auto_sync_label, setBezeled: NO]; - let _: () = msg_send![auto_sync_label, setDrawsBackground: NO]; - let _: () = msg_send![auto_sync_label, setEditable: NO]; - let _: () = msg_send![auto_sync_label, setSelectable: NO]; - let _: () = msg_send![content_view, addSubview: auto_sync_label]; - - let auto_sync_field: id = msg_send![NSTextField::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN + 110.0, current_y), - NSSize::new(310.0, FIELD_HEIGHT), - ) - ]; - let _: () = msg_send![auto_sync_field, setStringValue: NSString::alloc(nil).init_str(&settings.auto_sync_interval_minutes.to_string())]; - let _: () = msg_send![auto_sync_field, setEditable: YES]; - let _: () = msg_send![auto_sync_field, setSelectable: YES]; - let _: () = msg_send![auto_sync_field, setBezeled: YES]; - let _: () = msg_send![auto_sync_field, setBezelStyle: NSTextFieldSquareBezel]; - let _: () = msg_send![content_view, addSubview: auto_sync_field]; - - current_y -= 40.0; - - // In place checkbox - let in_place_checkbox: id = msg_send![NSButton::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(MARGIN, current_y), - NSSize::new(400.0, 24.0), - ) - ]; - let _: () = msg_send![in_place_checkbox, setButtonType: NS_SWITCH_BUTTON]; - let _: () = msg_send![in_place_checkbox, setTitle: NSString::alloc(nil).init_str("Update known_hosts file in-place after sync")]; - let _: () = msg_send![in_place_checkbox, setState: if settings.in_place { NS_CONTROL_STATE_VALUE_ON } else { NS_CONTROL_STATE_VALUE_OFF }]; - let _: () = msg_send![content_view, addSubview: in_place_checkbox]; - - // Save button - let save_button: id = msg_send![NSButton::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(WINDOW_WIDTH - 180.0, MARGIN), - NSSize::new(80.0, BUTTON_HEIGHT), - ) - ]; - let _: () = msg_send![save_button, setTitle: NSString::alloc(nil).init_str("Save")]; - let _: () = msg_send![content_view, addSubview: save_button]; - - // Cancel button - let cancel_button: id = msg_send![NSButton::alloc(nil), - initWithFrame: NSRect::new( - NSPoint::new(WINDOW_WIDTH - 90.0, MARGIN), - NSSize::new(80.0, BUTTON_HEIGHT), - ) - ]; - let _: () = msg_send![cancel_button, setTitle: NSString::alloc(nil).init_str("Cancel")]; - let _: () = msg_send![content_view, addSubview: cancel_button]; - - info!("All KHM UI elements created successfully"); - - Self { - window, - host_field, - flow_field, - known_hosts_field, - basic_auth_field, - auto_sync_field, - in_place_checkbox, - } - } - } - - fn collect_settings(&self) -> KhmSettings { - unsafe { - // Get host - let host_ns_string: id = msg_send![self.host_field, stringValue]; - let host_ptr: *const i8 = msg_send![host_ns_string, UTF8String]; - let host = CStr::from_ptr(host_ptr).to_string_lossy().to_string(); - - // Get flow - let flow_ns_string: id = msg_send![self.flow_field, stringValue]; - let flow_ptr: *const i8 = msg_send![flow_ns_string, UTF8String]; - let flow = CStr::from_ptr(flow_ptr).to_string_lossy().to_string(); - - // Get known hosts path - let known_hosts_ns_string: id = msg_send![self.known_hosts_field, stringValue]; - let known_hosts_ptr: *const i8 = msg_send![known_hosts_ns_string, UTF8String]; - let known_hosts = CStr::from_ptr(known_hosts_ptr).to_string_lossy().to_string(); - - // Get basic auth - let basic_auth_ns_string: id = msg_send![self.basic_auth_field, stringValue]; - let basic_auth_ptr: *const i8 = msg_send![basic_auth_ns_string, UTF8String]; - let basic_auth = CStr::from_ptr(basic_auth_ptr).to_string_lossy().to_string(); - - // Get auto sync interval - let auto_sync_ns_string: id = msg_send![self.auto_sync_field, stringValue]; - let auto_sync_ptr: *const i8 = msg_send![auto_sync_ns_string, UTF8String]; - let auto_sync_str = CStr::from_ptr(auto_sync_ptr).to_string_lossy().to_string(); - let auto_sync_interval_minutes = auto_sync_str.parse::().unwrap_or(60); // Default to 60 if parse fails - - // Get checkbox state - let in_place_state: i32 = msg_send![self.in_place_checkbox, state]; - let in_place = in_place_state == NS_CONTROL_STATE_VALUE_ON; - - KhmSettings { - host, - flow, - known_hosts, - basic_auth, - in_place, - auto_sync_interval_minutes, - } - } - } -} - -pub fn run_settings_window() { - info!("Starting native macOS KHM settings window"); - unsafe { - let pool = NSAutoreleasePool::new(nil); - let app = NSApp(); - info!("NSApp created for settings window"); - - // Set activation policy to regular for this standalone window - let _: () = msg_send![app, setActivationPolicy: 0]; // NSApplicationActivationPolicyRegular - info!("Activation policy set to Regular for settings window"); - - let settings_window = MacOSKhmSettingsWindow::new(); - let window = settings_window.window; - info!("KHM settings window created"); - - // Show window and activate app - let _: () = msg_send![app, activateIgnoringOtherApps: YES]; - let _: () = msg_send![window, makeKeyAndOrderFront: nil]; - let _: () = msg_send![window, orderFrontRegardless]; - info!("Settings window should be visible now"); - - // Run event loop until window is closed - let mut should_close = false; - while !should_close { - let event: id = msg_send![app, - nextEventMatchingMask: NSEventMask::NSAnyEventMask.bits() - untilDate: nil - inMode: NSDefaultRunLoopMode - dequeue: YES - ]; - - if event == nil { - continue; - } - - let event_type: NSEventType = msg_send![event, type]; - - // Handle window close button - if event_type == NSEventType::NSLeftMouseDown { - let event_window: id = msg_send![event, window]; - if event_window == window { - let location: NSPoint = msg_send![event, locationInWindow]; - - // Check if click is on Save button - if location.x >= WINDOW_WIDTH - 180.0 && location.x <= WINDOW_WIDTH - 100.0 && - location.y >= MARGIN && location.y <= MARGIN + BUTTON_HEIGHT { - info!("Save button clicked"); - let settings = settings_window.collect_settings(); - if let Err(e) = save_settings(&settings) { - error!("Failed to save KHM settings: {}", e); - } else { - info!("KHM settings saved from native macOS window"); - } - should_close = true; - continue; - } - - // Check if click is on Cancel button - if location.x >= WINDOW_WIDTH - 90.0 && location.x <= WINDOW_WIDTH - 10.0 && - location.y >= MARGIN && location.y <= MARGIN + BUTTON_HEIGHT { - info!("Cancel button clicked"); - should_close = true; - continue; - } - } - } - - // Check if window is closed via close button or ESC - if event_type == NSEventType::NSKeyDown { - let key_code: u16 = msg_send![event, keyCode]; - let flags: NSEventModifierFlags = msg_send![event, modifierFlags]; - - // Handle Cmd+V (paste), Cmd+C (copy), Cmd+X (cut), Cmd+A (select all) - if flags.contains(NSEventModifierFlags::NSCommandKeyMask) { - match key_code { - 9 => { // V key - paste - let responder: id = msg_send![window, firstResponder]; - let _: () = msg_send![responder, paste: nil]; - continue; - } - 8 => { // C key - copy - let responder: id = msg_send![window, firstResponder]; - let _: () = msg_send![responder, copy: nil]; - continue; - } - 7 => { // X key - cut - let responder: id = msg_send![window, firstResponder]; - let _: () = msg_send![responder, cut: nil]; - continue; - } - 0 => { // A key - select all - let responder: id = msg_send![window, firstResponder]; - let _: () = msg_send![responder, selectAll: nil]; - continue; - } - _ => {} - } - } - - if key_code == 53 { // ESC key - info!("ESC pressed, closing settings window"); - should_close = true; - continue; - } - } - - // Forward event to application - let _: () = msg_send![app, sendEvent: event]; - } - - let _: () = msg_send![window, close]; - info!("Native macOS KHM settings window closed"); - - pool.drain(); - } -} diff --git a/src/gui/settings/mod.rs b/src/gui/settings/mod.rs index b3703de..4582f9c 100644 --- a/src/gui/settings/mod.rs +++ b/src/gui/settings/mod.rs @@ -29,11 +29,11 @@ pub struct SshKey { #[derive(Debug, Clone)] enum AdminOperation { LoadingKeys, - DeprecatingKey(String), - RestoringKey(String), - DeletingKey(String), - BulkDeprecating(Vec), - BulkRestoring(Vec), + DeprecatingKey, + RestoringKey, + DeletingKey, + BulkDeprecating, + BulkRestoring, None, } @@ -275,14 +275,14 @@ impl eframe::App for KhmSettingsWindow { .show(ctx, |ui| { // Header with title ui.horizontal(|ui| { - ui.heading(egui::RichText::new("🔐 KHM Settings").size(24.0)); + ui.heading(egui::RichText::new("🔑 KHM Settings").size(24.0)); }); ui.add_space(10.0); // Tab selector ui.horizontal(|ui| { - ui.selectable_value(&mut self.current_tab, SettingsTab::Connection, "⚙ Connection"); + ui.selectable_value(&mut self.current_tab, SettingsTab::Connection, "📃 Settings"); ui.selectable_value(&mut self.current_tab, SettingsTab::Admin, "🔧 Admin"); }); @@ -318,7 +318,7 @@ impl KhmSettingsWindow { ui.label(egui::RichText::new("Not tested").color(egui::Color32::GRAY)); } ConnectionStatus::Connected { keys_count, flow } => { - ui.label(egui::RichText::new("✓").color(egui::Color32::GREEN)); + ui.label(egui::RichText::new("✅").color(egui::Color32::GREEN)); ui.label(egui::RichText::new(format!("{} keys in '{}'", keys_count, flow)) .color(egui::Color32::LIGHT_GREEN)); } @@ -391,7 +391,7 @@ impl KhmSettingsWindow { }); ui.add_space(8.0); - ui.checkbox(&mut self.settings.in_place, "✏️ Update known_hosts file in-place after sync"); + ui.checkbox(&mut self.settings.in_place, "✏ Update known_hosts file in-place after sync"); }); }); @@ -423,9 +423,9 @@ impl KhmSettingsWindow { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if is_auto_sync_enabled { - ui.label(egui::RichText::new("✅ Enabled").color(egui::Color32::GREEN)); + ui.label(egui::RichText::new("🔄 Enabled").color(egui::Color32::GREEN)); } else { - ui.label(egui::RichText::new("⏸️ Disabled").color(egui::Color32::YELLOW)); + ui.label(egui::RichText::new("❌ Disabled").color(egui::Color32::YELLOW)); ui.label("(Configure host, flow & enable in-place sync)"); } }); @@ -483,7 +483,7 @@ impl KhmSettingsWindow { } if ui.add( - egui::Button::new("❌ Cancel") + egui::Button::new("✖ Cancel") .min_size(egui::vec2(80.0, 32.0)) ).clicked() { ctx.send_viewport_cmd(egui::ViewportCommand::Close); @@ -496,9 +496,9 @@ impl KhmSettingsWindow { can_test, egui::Button::new( if self.is_testing_connection { - "🔄 Testing..." + "▶ Testing..." } else { - "🧪 Test Connection" + "🔍 Test Connection" } ).min_size(egui::vec2(120.0, 32.0)) ).clicked() { @@ -510,7 +510,7 @@ impl KhmSettingsWindow { // Show validation hints if !save_enabled { ui.add_space(5.0); - ui.label(egui::RichText::new("⚠️ Please fill in Host URL and Flow Name to save settings") + ui.label(egui::RichText::new("❗ Please fill in Host URL and Flow Name to save settings") .color(egui::Color32::YELLOW) .italics()); } @@ -549,7 +549,7 @@ impl KhmSettingsWindow { ui.label(egui::RichText::new("🔧 Admin Panel").size(18.0).strong()); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button("🔄 Refresh").clicked() { + if ui.button("🔁 Refresh").clicked() { self.load_admin_keys(ctx); } @@ -566,7 +566,7 @@ impl KhmSettingsWindow { // Check if connection is configured if self.settings.host.is_empty() || self.settings.flow.is_empty() { ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("⚠ Please configure connection settings first") + ui.label(egui::RichText::new("❗ Please configure connection settings first") .size(16.0) .color(egui::Color32::YELLOW)); ui.add_space(10.0); @@ -603,50 +603,50 @@ impl KhmSettingsWindow { let deprecated_keys = total_keys - active_keys; let unique_servers = self.admin_state.keys.iter().map(|k| &k.server).collect::>().len(); - egui::Grid::new("stats_grid") - .num_columns(4) - .min_col_width(80.0) - .spacing([15.0, 8.0]) - .show(ui, |ui| { + ui.horizontal(|ui| { + ui.columns(4, |cols| { // Total keys - ui.vertical_centered(|ui| { + cols[0].vertical_centered_justified(|ui| { ui.label(egui::RichText::new("📊").size(20.0)); ui.label(egui::RichText::new(total_keys.to_string()).size(24.0).strong()); ui.label(egui::RichText::new("Total Keys").size(11.0).color(egui::Color32::GRAY)); }); // Active keys - ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("✓").size(20.0)); + cols[1].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("✅").size(20.0)); ui.label(egui::RichText::new(active_keys.to_string()).size(24.0).strong().color(egui::Color32::LIGHT_GREEN)); ui.label(egui::RichText::new("Active").size(11.0).color(egui::Color32::GRAY)); }); // Deprecated keys - ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("⚠").size(20.0)); + cols[2].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("❌").size(20.0)); ui.label(egui::RichText::new(deprecated_keys.to_string()).size(24.0).strong().color(egui::Color32::LIGHT_RED)); ui.label(egui::RichText::new("Deprecated").size(11.0).color(egui::Color32::GRAY)); }); // Servers - ui.vertical_centered(|ui| { - ui.label(egui::RichText::new("🖥").size(20.0)); + cols[3].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("💻").size(20.0)); ui.label(egui::RichText::new(unique_servers.to_string()).size(24.0).strong().color(egui::Color32::LIGHT_BLUE)); ui.label(egui::RichText::new("Servers").size(11.0).color(egui::Color32::GRAY)); }); - - ui.end_row(); }); + }); }); }); ui.add_space(10.0); - // Enhanced search and filters - более компактные + // Enhanced search and filters - адаптивный подход как в блоках статистики ui.group(|ui| { + ui.set_min_width(ui.available_width()); ui.vertical(|ui| { - // Первая строка - поиск + ui.label(egui::RichText::new("🔍 Search").size(16.0).strong()); + ui.add_space(8.0); + + // Search field with full width like statistics blocks ui.horizontal(|ui| { ui.label(egui::RichText::new("🔍").size(14.0)); let search_response = ui.add_sized( @@ -659,7 +659,7 @@ impl KhmSettingsWindow { ui.label(egui::RichText::new("Type to search").size(11.0).color(egui::Color32::GRAY)); } else { ui.label(egui::RichText::new(format!("{} results", self.admin_state.filtered_keys.len())).size(11.0)); - if ui.add(egui::Button::new(egui::RichText::new("X").color(egui::Color32::WHITE)) + if ui.add(egui::Button::new(egui::RichText::new("❌").color(egui::Color32::WHITE)) .fill(egui::Color32::from_rgb(170, 170, 170)) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(89, 89, 89))) .rounding(egui::Rounding::same(3.0)) @@ -682,11 +682,11 @@ impl KhmSettingsWindow { ui.horizontal(|ui| { ui.label("Filter:"); let show_deprecated = self.admin_state.show_deprecated_only; - if ui.selectable_label(!show_deprecated, "✓ Active").clicked() { + if ui.selectable_label(!show_deprecated, "✅ Active").clicked() { self.admin_state.show_deprecated_only = false; self.filter_admin_keys(); } - if ui.selectable_label(show_deprecated, "⚠ Deprecated").clicked() { + if ui.selectable_label(show_deprecated, "❗ Deprecated").clicked() { self.admin_state.show_deprecated_only = true; self.filter_admin_keys(); } @@ -713,7 +713,7 @@ impl KhmSettingsWindow { ui.add_space(5.0); ui.horizontal(|ui| { - if ui.add(egui::Button::new(egui::RichText::new("⚠ Deprecate Selected").color(egui::Color32::BLACK)) + if ui.add(egui::Button::new(egui::RichText::new("❗ Deprecate Selected").color(egui::Color32::BLACK)) .fill(egui::Color32::from_rgb(255, 200, 0)) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(102, 94, 72))) .rounding(egui::Rounding::same(6.0)) @@ -722,7 +722,7 @@ impl KhmSettingsWindow { self.deprecate_selected_servers(ctx); } - if ui.add(egui::Button::new(egui::RichText::new("✓ Restore Selected").color(egui::Color32::WHITE)) + if ui.add(egui::Button::new(egui::RichText::new("✅ Restore Selected").color(egui::Color32::WHITE)) .fill(egui::Color32::from_rgb(101, 199, 40)) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(94, 105, 25))) .rounding(egui::Rounding::same(6.0)) @@ -826,7 +826,7 @@ impl KhmSettingsWindow { .size(14.0) .color(egui::Color32::DARK_GRAY)); } else { - ui.label(egui::RichText::new("X").size(48.0).color(egui::Color32::GRAY)); + ui.label(egui::RichText::new("❌").size(48.0).color(egui::Color32::GRAY)); ui.label(egui::RichText::new("No keys match current filters") .size(18.0) .color(egui::Color32::GRAY)); @@ -866,7 +866,7 @@ impl KhmSettingsWindow { } // Modern expand/collapse button - let expand_icon = if is_expanded { "▼" } else { "▶" }; + let expand_icon = if is_expanded { "🔺" } else { "🔻" }; if ui.add(egui::Button::new(expand_icon) .fill(egui::Color32::TRANSPARENT) .stroke(egui::Stroke::NONE) @@ -876,7 +876,7 @@ impl KhmSettingsWindow { } // Server icon and name - ui.label(egui::RichText::new("🖥").size(16.0)); + ui.label(egui::RichText::new("💻").size(16.0)); ui.label(egui::RichText::new(&server_name) .size(15.0) .strong() @@ -925,7 +925,7 @@ impl KhmSettingsWindow { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Stylized action buttons - improved colors if deprecated_count > 0 { - if ui.add(egui::Button::new(egui::RichText::new("✓ Restore").color(egui::Color32::WHITE)) + if ui.add(egui::Button::new(egui::RichText::new("✅ Restore").color(egui::Color32::WHITE)) .fill(egui::Color32::from_rgb(101, 199, 40)) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(94, 105, 25))) .rounding(egui::Rounding::same(4.0)) @@ -936,7 +936,7 @@ impl KhmSettingsWindow { } if active_count > 0 { - if ui.add(egui::Button::new(egui::RichText::new("⚠ Deprecate").color(egui::Color32::BLACK)) + if ui.add(egui::Button::new(egui::RichText::new("❗ Deprecate").color(egui::Color32::BLACK)) .fill(egui::Color32::from_rgb(255, 200, 0)) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(102, 94, 72))) .rounding(egui::Rounding::same(4.0)) @@ -987,12 +987,12 @@ impl KhmSettingsWindow { // Status badge with icons - меньшие if key.deprecated { - ui.label(egui::RichText::new("⚠ DEPR") + ui.label(egui::RichText::new("❗ DEPR") .size(10.0) .color(egui::Color32::from_rgb(231, 76, 60)) .strong()); } else { - ui.label(egui::RichText::new("✓ ACTIVE") + ui.label(egui::RichText::new("[OK] ACTIVE") .size(10.0) .color(egui::Color32::from_rgb(46, 204, 113)) .strong()); @@ -1009,7 +1009,7 @@ impl KhmSettingsWindow { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Modern action buttons - improved colors if key.deprecated { - if ui.add(egui::Button::new(egui::RichText::new("✓").color(egui::Color32::WHITE)) + if ui.add(egui::Button::new(egui::RichText::new("[R]").color(egui::Color32::WHITE)) .fill(egui::Color32::from_rgb(101, 199, 40)) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(94, 105, 25))) .rounding(egui::Rounding::same(3.0)) @@ -1026,7 +1026,7 @@ impl KhmSettingsWindow { self.delete_key(&server_name_for_action, ctx); } } else { - if ui.add(egui::Button::new(egui::RichText::new("⚠").color(egui::Color32::BLACK)) + if ui.add(egui::Button::new(egui::RichText::new("❗").color(egui::Color32::BLACK)) .fill(egui::Color32::from_rgb(255, 200, 0)) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(102, 94, 72))) .rounding(egui::Rounding::same(3.0)) @@ -1055,12 +1055,66 @@ impl KhmSettingsWindow { } } - fn deprecate_selected_servers(&mut self, _ctx: &egui::Context) { - // Stub for now + fn deprecate_selected_servers(&mut self, ctx: &egui::Context) { + let selected: Vec = self.admin_state.selected_servers + .iter() + .filter_map(|(server, &selected)| if selected { Some(server.clone()) } else { None }) + .collect(); + + if selected.is_empty() { + return; + } + + self.admin_state.current_operation = AdminOperation::BulkDeprecating; + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + bulk_deprecate_servers(host, flow, basic_auth, selected).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); } - fn restore_selected_servers(&mut self, _ctx: &egui::Context) { - // Stub for now + fn restore_selected_servers(&mut self, ctx: &egui::Context) { + let selected: Vec = self.admin_state.selected_servers + .iter() + .filter_map(|(server, &selected)| if selected { Some(server.clone()) } else { None }) + .collect(); + + if selected.is_empty() { + return; + } + + self.admin_state.current_operation = AdminOperation::BulkRestoring; + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + bulk_restore_servers(host, flow, basic_auth, selected).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); } fn get_key_type(&self, public_key: &str) -> String { @@ -1091,24 +1145,81 @@ impl KhmSettingsWindow { } } - fn deprecate_server_keys(&mut self, _server: &str, _ctx: &egui::Context) { - // Stub for now + fn deprecate_server_keys(&mut self, server: &str, ctx: &egui::Context) { + self.admin_state.current_operation = AdminOperation::DeprecatingKey; + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let server_name = server.to_string(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + deprecate_key_by_server(host, flow, basic_auth, server_name).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); } - fn restore_server_keys(&mut self, _server: &str, _ctx: &egui::Context) { - // Stub for now + fn restore_server_keys(&mut self, server: &str, ctx: &egui::Context) { + self.admin_state.current_operation = AdminOperation::RestoringKey; + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let server_name = server.to_string(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + restore_key_by_server(host, flow, basic_auth, server_name).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); } - fn deprecate_key(&mut self, _server: &str, _ctx: &egui::Context) { - // Stub for now + fn deprecate_key(&mut self, server: &str, ctx: &egui::Context) { + self.deprecate_server_keys(server, ctx); } - fn restore_key(&mut self, _server: &str, _ctx: &egui::Context) { - // Stub for now + fn restore_key(&mut self, server: &str, ctx: &egui::Context) { + self.restore_server_keys(server, ctx); } - fn delete_key(&mut self, _server: &str, _ctx: &egui::Context) { - // Stub for now + fn delete_key(&mut self, server: &str, ctx: &egui::Context) { + self.admin_state.current_operation = AdminOperation::DeletingKey; + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let server_name = server.to_string(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + permanently_delete_key_by_server(host, flow, basic_auth, server_name).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); } } @@ -1172,6 +1283,248 @@ fn create_window_icon() -> egui::IconData { } } +// Admin API functions +async fn bulk_deprecate_servers(host: String, flow: String, basic_auth: String, servers: Vec) -> Result { + if host.is_empty() || flow.is_empty() { + return Err("Host and flow must be specified".to_string()); + } + + let url = format!("{}/{}/bulk-deprecate", host.trim_end_matches('/'), flow); + info!("Bulk deprecating {} servers at: {}", servers.len(), url); + + let client_builder = Client::builder() + .timeout(std::time::Duration::from_secs(30)); + + let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; + + let mut request = client.post(&url) + .json(&serde_json::json!({ + "servers": servers + })); + + // Add basic auth if provided + if !basic_auth.is_empty() { + let auth_parts: Vec<&str> = basic_auth.splitn(2, ':').collect(); + if auth_parts.len() == 2 { + request = request.basic_auth(auth_parts[0], Some(auth_parts[1])); + } else { + return Err("Basic auth format should be 'username:password'".to_string()); + } + } + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); + } + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + // Parse JSON response to get message + if let Ok(json_response) = serde_json::from_str::(&body) { + if let Some(message) = json_response.get("message").and_then(|v| v.as_str()) { + Ok(message.to_string()) + } else { + Ok("Successfully deprecated servers".to_string()) + } + } else { + Ok("Successfully deprecated servers".to_string()) + } +} + +async fn bulk_restore_servers(host: String, flow: String, basic_auth: String, servers: Vec) -> Result { + if host.is_empty() || flow.is_empty() { + return Err("Host and flow must be specified".to_string()); + } + + let url = format!("{}/{}/bulk-restore", host.trim_end_matches('/'), flow); + info!("Bulk restoring {} servers at: {}", servers.len(), url); + + let client_builder = Client::builder() + .timeout(std::time::Duration::from_secs(30)); + + let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; + + let mut request = client.post(&url) + .json(&serde_json::json!({ + "servers": servers + })); + + // Add basic auth if provided + if !basic_auth.is_empty() { + let auth_parts: Vec<&str> = basic_auth.splitn(2, ':').collect(); + if auth_parts.len() == 2 { + request = request.basic_auth(auth_parts[0], Some(auth_parts[1])); + } else { + return Err("Basic auth format should be 'username:password'".to_string()); + } + } + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); + } + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + // Parse JSON response to get message + if let Ok(json_response) = serde_json::from_str::(&body) { + if let Some(message) = json_response.get("message").and_then(|v| v.as_str()) { + Ok(message.to_string()) + } else { + Ok("Successfully restored servers".to_string()) + } + } else { + Ok("Successfully restored servers".to_string()) + } +} + +async fn deprecate_key_by_server(host: String, flow: String, basic_auth: String, server: String) -> Result { + if host.is_empty() || flow.is_empty() { + return Err("Host and flow must be specified".to_string()); + } + + let url = format!("{}/{}/keys/{}", host.trim_end_matches('/'), flow, urlencoding::encode(&server)); + info!("Deprecating key for server '{}' at: {}", server, url); + + let client_builder = Client::builder() + .timeout(std::time::Duration::from_secs(30)); + + let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; + + let mut request = client.delete(&url); + + // Add basic auth if provided + if !basic_auth.is_empty() { + let auth_parts: Vec<&str> = basic_auth.splitn(2, ':').collect(); + if auth_parts.len() == 2 { + request = request.basic_auth(auth_parts[0], Some(auth_parts[1])); + } else { + return Err("Basic auth format should be 'username:password'".to_string()); + } + } + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); + } + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + // Parse JSON response to get message + if let Ok(json_response) = serde_json::from_str::(&body) { + if let Some(message) = json_response.get("message").and_then(|v| v.as_str()) { + Ok(message.to_string()) + } else { + Ok(format!("Successfully deprecated key for server '{}'", server)) + } + } else { + Ok(format!("Successfully deprecated key for server '{}'", server)) + } +} + +async fn restore_key_by_server(host: String, flow: String, basic_auth: String, server: String) -> Result { + if host.is_empty() || flow.is_empty() { + return Err("Host and flow must be specified".to_string()); + } + + let url = format!("{}/{}/keys/{}/restore", host.trim_end_matches('/'), flow, urlencoding::encode(&server)); + info!("Restoring key for server '{}' at: {}", server, url); + + let client_builder = Client::builder() + .timeout(std::time::Duration::from_secs(30)); + + let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; + + let mut request = client.post(&url); + + // Add basic auth if provided + if !basic_auth.is_empty() { + let auth_parts: Vec<&str> = basic_auth.splitn(2, ':').collect(); + if auth_parts.len() == 2 { + request = request.basic_auth(auth_parts[0], Some(auth_parts[1])); + } else { + return Err("Basic auth format should be 'username:password'".to_string()); + } + } + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); + } + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + // Parse JSON response to get message + if let Ok(json_response) = serde_json::from_str::(&body) { + if let Some(message) = json_response.get("message").and_then(|v| v.as_str()) { + Ok(message.to_string()) + } else { + Ok(format!("Successfully restored key for server '{}'", server)) + } + } else { + Ok(format!("Successfully restored key for server '{}'", server)) + } +} + +async fn permanently_delete_key_by_server(host: String, flow: String, basic_auth: String, server: String) -> Result { + if host.is_empty() || flow.is_empty() { + return Err("Host and flow must be specified".to_string()); + } + + let url = format!("{}/{}/keys/{}/delete", host.trim_end_matches('/'), flow, urlencoding::encode(&server)); + info!("Permanently deleting key for server '{}' at: {}", server, url); + + let client_builder = Client::builder() + .timeout(std::time::Duration::from_secs(30)); + + let client = client_builder.build().map_err(|e| format!("Failed to create HTTP client: {}", e))?; + + let mut request = client.delete(&url); + + // Add basic auth if provided + if !basic_auth.is_empty() { + let auth_parts: Vec<&str> = basic_auth.splitn(2, ':').collect(); + if auth_parts.len() == 2 { + request = request.basic_auth(auth_parts[0], Some(auth_parts[1])); + } else { + return Err("Basic auth format should be 'username:password'".to_string()); + } + } + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Server returned error: {} {}", response.status().as_u16(), response.status().canonical_reason().unwrap_or("Unknown"))); + } + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + // Parse JSON response to get message + if let Ok(json_response) = serde_json::from_str::(&body) { + if let Some(message) = json_response.get("message").and_then(|v| v.as_str()) { + Ok(message.to_string()) + } else { + Ok(format!("Successfully deleted key for server '{}'", server)) + } + } else { + Ok(format!("Successfully deleted key for server '{}'", server)) + } +} + async fn test_khm_connection(host: String, flow: String, basic_auth: String) -> Result { if host.is_empty() || flow.is_empty() { return Err("Host and flow must be specified".to_string());