mirror of
https://github.com/house-of-vanity/khm.git
synced 2025-08-21 14:27:14 +00:00
GUI Feature
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -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]]
|
||||
|
@@ -29,4 +29,5 @@ eframe = "0.29"
|
||||
egui = "0.29"
|
||||
winit = "0.30"
|
||||
env_logger = "0.11"
|
||||
urlencoding = "2.1"
|
||||
|
||||
|
@@ -425,7 +425,7 @@ impl ApplicationHandler<UserEvent> for Application {
|
||||
}
|
||||
}
|
||||
|
||||
tray_icon.set_tooltip(Some(&tooltip));
|
||||
let _ = tray_icon.set_tooltip(Some(&tooltip));
|
||||
}
|
||||
}
|
||||
drop(settings);
|
||||
@@ -511,7 +511,7 @@ impl ApplicationHandler<UserEvent> for Application {
|
||||
}
|
||||
}
|
||||
|
||||
tray_icon.set_tooltip(Some(&tooltip));
|
||||
let _ = tray_icon.set_tooltip(Some(&tooltip));
|
||||
}
|
||||
|
||||
// Restart auto sync if interval changed or settings changed
|
||||
|
@@ -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 }))),
|
||||
);
|
||||
}
|
@@ -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::<u32>().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();
|
||||
}
|
||||
}
|
@@ -29,11 +29,11 @@ pub struct SshKey {
|
||||
#[derive(Debug, Clone)]
|
||||
enum AdminOperation {
|
||||
LoadingKeys,
|
||||
DeprecatingKey(String),
|
||||
RestoringKey(String),
|
||||
DeletingKey(String),
|
||||
BulkDeprecating(Vec<String>),
|
||||
BulkRestoring(Vec<String>),
|
||||
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::<std::collections::HashSet<_>>().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<String> = 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<String> = 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<String>) -> Result<String, String> {
|
||||
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::<serde_json::Value>(&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<String>) -> Result<String, String> {
|
||||
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::<serde_json::Value>(&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<String, String> {
|
||||
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::<serde_json::Value>(&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<String, String> {
|
||||
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::<serde_json::Value>(&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<String, String> {
|
||||
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::<serde_json::Value>(&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<String, String> {
|
||||
if host.is_empty() || flow.is_empty() {
|
||||
return Err("Host and flow must be specified".to_string());
|
||||
|
Reference in New Issue
Block a user