GUI Feature

This commit is contained in:
Ultradesu
2025-07-22 16:20:39 +03:00
parent 2e5cf1ca29
commit d604bb8119
6 changed files with 429 additions and 557 deletions

17
Cargo.lock generated
View File

@@ -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]]

View File

@@ -29,4 +29,5 @@ eframe = "0.29"
egui = "0.29"
winit = "0.30"
env_logger = "0.11"
urlencoding = "2.1"

View File

@@ -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

View File

@@ -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 }))),
);
}

View File

@@ -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();
}
}

View File

@@ -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());