mirror of
https://github.com/house-of-vanity/khm.git
synced 2025-08-22 14:37:15 +00:00
UI code reworked
This commit is contained in:
190
src/gui/settings/connection.rs
Normal file
190
src/gui/settings/connection.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use eframe::egui;
|
||||
use log::{error, info};
|
||||
use std::sync::mpsc;
|
||||
use crate::gui::api::{test_connection, perform_manual_sync};
|
||||
use crate::gui::common::{KhmSettings, save_settings};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ConnectionStatus {
|
||||
Unknown,
|
||||
Connected { keys_count: usize, flow: String },
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SyncStatus {
|
||||
Unknown,
|
||||
Success { keys_count: usize },
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SettingsTab {
|
||||
Connection,
|
||||
Admin,
|
||||
}
|
||||
|
||||
pub struct ConnectionTab {
|
||||
pub connection_status: ConnectionStatus,
|
||||
pub is_testing_connection: bool,
|
||||
pub test_result_receiver: Option<mpsc::Receiver<Result<String, String>>>,
|
||||
pub is_syncing: bool,
|
||||
pub sync_result_receiver: Option<mpsc::Receiver<Result<String, String>>>,
|
||||
pub sync_status: SyncStatus,
|
||||
}
|
||||
|
||||
impl Default for ConnectionTab {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
connection_status: ConnectionStatus::Unknown,
|
||||
is_testing_connection: false,
|
||||
test_result_receiver: None,
|
||||
is_syncing: false,
|
||||
sync_result_receiver: None,
|
||||
sync_status: SyncStatus::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionTab {
|
||||
/// Start connection test
|
||||
pub fn start_test(&mut self, settings: &KhmSettings, ctx: &egui::Context) {
|
||||
if self.is_testing_connection {
|
||||
return;
|
||||
}
|
||||
|
||||
self.is_testing_connection = true;
|
||||
self.connection_status = ConnectionStatus::Unknown;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.test_result_receiver = Some(rx);
|
||||
|
||||
let host = settings.host.clone();
|
||||
let flow = settings.flow.clone();
|
||||
let basic_auth = 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 {
|
||||
test_connection(host, flow, basic_auth).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/// Start manual sync
|
||||
pub fn start_sync(&mut self, settings: &KhmSettings, ctx: &egui::Context) {
|
||||
if self.is_syncing {
|
||||
return;
|
||||
}
|
||||
|
||||
self.is_syncing = true;
|
||||
self.sync_status = SyncStatus::Unknown;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.sync_result_receiver = Some(rx);
|
||||
|
||||
let settings = settings.clone();
|
||||
let ctx_clone = ctx.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let result = rt.block_on(async {
|
||||
perform_manual_sync(settings).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/// Check for test/sync results
|
||||
pub fn check_results(&mut self, ctx: &egui::Context, settings: &KhmSettings) {
|
||||
// Check for test connection result
|
||||
if let Some(receiver) = &self.test_result_receiver {
|
||||
if let Ok(result) = receiver.try_recv() {
|
||||
self.is_testing_connection = false;
|
||||
match result {
|
||||
Ok(message) => {
|
||||
// Parse keys count from message
|
||||
let keys_count = if let Some(start) = message.find("Found ") {
|
||||
if let Some(end) = message[start + 6..].find(" SSH keys") {
|
||||
message[start + 6..start + 6 + end].parse::<usize>().unwrap_or(0)
|
||||
} else { 0 }
|
||||
} else { 0 };
|
||||
|
||||
self.connection_status = ConnectionStatus::Connected {
|
||||
keys_count,
|
||||
flow: settings.flow.clone()
|
||||
};
|
||||
info!("Connection test successful: {}", message);
|
||||
}
|
||||
Err(error) => {
|
||||
self.connection_status = ConnectionStatus::Error(error);
|
||||
error!("Connection test failed");
|
||||
}
|
||||
}
|
||||
self.test_result_receiver = None;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for sync result
|
||||
if let Some(receiver) = &self.sync_result_receiver {
|
||||
if let Ok(result) = receiver.try_recv() {
|
||||
self.is_syncing = false;
|
||||
match result {
|
||||
Ok(message) => {
|
||||
// Parse keys count from message
|
||||
let keys_count = parse_keys_count(&message);
|
||||
self.sync_status = SyncStatus::Success { keys_count };
|
||||
info!("Sync successful: {}", message);
|
||||
}
|
||||
Err(error) => {
|
||||
self.sync_status = SyncStatus::Error(error);
|
||||
error!("Sync failed");
|
||||
}
|
||||
}
|
||||
self.sync_result_receiver = None;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse keys count from sync result message
|
||||
fn parse_keys_count(message: &str) -> usize {
|
||||
if let Some(start) = message.find("updated with ") {
|
||||
let search_start = start + "updated with ".len();
|
||||
if let Some(end) = message[search_start..].find(" keys") {
|
||||
let number_str = &message[search_start..search_start + end];
|
||||
return number_str.parse::<usize>().unwrap_or(0);
|
||||
}
|
||||
} else if let Some(start) = message.find("Retrieved ") {
|
||||
let search_start = start + "Retrieved ".len();
|
||||
if let Some(end) = message[search_start..].find(" keys") {
|
||||
let number_str = &message[search_start..search_start + end];
|
||||
return number_str.parse::<usize>().unwrap_or(0);
|
||||
}
|
||||
} else if let Some(keys_pos) = message.find(" keys") {
|
||||
let before_keys = &message[..keys_pos];
|
||||
if let Some(space_pos) = before_keys.rfind(' ') {
|
||||
let number_str = &before_keys[space_pos + 1..];
|
||||
return number_str.parse::<usize>().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// Save settings with validation
|
||||
pub fn save_settings_validated(settings: &KhmSettings) -> Result<(), String> {
|
||||
if settings.host.is_empty() || settings.flow.is_empty() {
|
||||
return Err("Host URL and Flow Name are required".to_string());
|
||||
}
|
||||
|
||||
save_settings(settings).map_err(|e| format!("Failed to save settings: {}", e))
|
||||
}
|
File diff suppressed because it is too large
Load Diff
356
src/gui/settings/ui.rs
Normal file
356
src/gui/settings/ui.rs
Normal file
@@ -0,0 +1,356 @@
|
||||
use eframe::egui;
|
||||
use crate::gui::common::{KhmSettings, get_config_path};
|
||||
use super::connection::{ConnectionTab, ConnectionStatus, SyncStatus, save_settings_validated};
|
||||
|
||||
/// Render connection settings tab
|
||||
pub fn render_connection_tab(
|
||||
ui: &mut egui::Ui,
|
||||
ctx: &egui::Context,
|
||||
settings: &mut KhmSettings,
|
||||
auto_sync_interval_str: &mut String,
|
||||
connection_tab: &mut ConnectionTab,
|
||||
operation_log: &mut Vec<String>
|
||||
) {
|
||||
// Check for connection test and sync results
|
||||
connection_tab.check_results(ctx, settings);
|
||||
|
||||
let available_height = ui.available_height();
|
||||
let button_area_height = 120.0; // Reserve space for buttons and status
|
||||
let content_height = available_height - button_area_height;
|
||||
|
||||
// Main content area (scrollable)
|
||||
ui.allocate_ui_with_layout(
|
||||
[ui.available_width(), content_height].into(),
|
||||
egui::Layout::top_down(egui::Align::Min),
|
||||
|ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
// Connection section
|
||||
render_connection_section(ui, settings, connection_tab);
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Local settings section
|
||||
render_local_settings_section(ui, settings);
|
||||
ui.add_space(15.0);
|
||||
|
||||
// Auto-sync section
|
||||
render_auto_sync_section(ui, settings, auto_sync_interval_str);
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Configuration file location
|
||||
render_config_location_section(ui);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Bottom area with buttons and log
|
||||
render_bottom_area(ui, ctx, settings, connection_tab, operation_log);
|
||||
}
|
||||
|
||||
fn render_connection_section(ui: &mut egui::Ui, settings: &mut KhmSettings, connection_tab: &ConnectionTab) {
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("🌐 Connection").size(16.0).strong());
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Display connection status with details
|
||||
match &connection_tab.connection_status {
|
||||
ConnectionStatus::Connected { keys_count, flow } => {
|
||||
let status_text = if flow.is_empty() {
|
||||
format!("Connected ({} keys)", keys_count)
|
||||
} else {
|
||||
format!("Connected to '{}' ({} keys)", flow, keys_count)
|
||||
};
|
||||
ui.add_enabled(false, egui::Checkbox::new(&mut true, status_text));
|
||||
}
|
||||
ConnectionStatus::Error(error_msg) => {
|
||||
ui.label(egui::RichText::new("❌ Error").color(egui::Color32::RED))
|
||||
.on_hover_text(error_msg);
|
||||
}
|
||||
ConnectionStatus::Unknown => {
|
||||
ui.add_enabled(false, egui::Checkbox::new(&mut false, "Not connected"));
|
||||
}
|
||||
}
|
||||
|
||||
if connection_tab.is_testing_connection {
|
||||
ui.spinner();
|
||||
ui.label(egui::RichText::new("Testing...").italics());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Display sync status if available
|
||||
match &connection_tab.sync_status {
|
||||
SyncStatus::Success { keys_count } => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("🔄 Last sync:");
|
||||
ui.label(egui::RichText::new(format!("{} keys synced", keys_count))
|
||||
.color(egui::Color32::GREEN));
|
||||
});
|
||||
}
|
||||
SyncStatus::Error(error_msg) => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("🔄 Last sync:");
|
||||
ui.label(egui::RichText::new("Failed")
|
||||
.color(egui::Color32::RED))
|
||||
.on_hover_text(error_msg);
|
||||
});
|
||||
}
|
||||
SyncStatus::Unknown => {}
|
||||
}
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
egui::Grid::new("connection_grid")
|
||||
.num_columns(2)
|
||||
.min_col_width(120.0)
|
||||
.spacing([10.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
ui.label("Host URL:");
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 20.0],
|
||||
egui::TextEdit::singleline(&mut settings.host)
|
||||
.hint_text("https://your-khm-server.com")
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Flow Name:");
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 20.0],
|
||||
egui::TextEdit::singleline(&mut settings.flow)
|
||||
.hint_text("production, staging, etc.")
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Basic Auth:");
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 20.0],
|
||||
egui::TextEdit::singleline(&mut settings.basic_auth)
|
||||
.hint_text("username:password (optional)")
|
||||
.password(true)
|
||||
);
|
||||
ui.end_row();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_local_settings_section(ui: &mut egui::Ui, settings: &mut KhmSettings) {
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("📁 Local Settings").size(16.0).strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
egui::Grid::new("local_grid")
|
||||
.num_columns(2)
|
||||
.min_col_width(120.0)
|
||||
.spacing([10.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
ui.label("Known Hosts File:");
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 20.0],
|
||||
egui::TextEdit::singleline(&mut settings.known_hosts)
|
||||
.hint_text("~/.ssh/known_hosts")
|
||||
);
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.checkbox(&mut settings.in_place, "✏ Update known_hosts file in-place after sync");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_auto_sync_section(ui: &mut egui::Ui, settings: &mut KhmSettings, auto_sync_interval_str: &mut String) {
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("🔄 Auto Sync").size(16.0).strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
let is_auto_sync_enabled = !settings.host.is_empty()
|
||||
&& !settings.flow.is_empty()
|
||||
&& settings.in_place;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Interval (minutes):");
|
||||
ui.add_sized(
|
||||
[80.0, 20.0],
|
||||
egui::TextEdit::singleline(auto_sync_interval_str)
|
||||
);
|
||||
|
||||
if let Ok(value) = auto_sync_interval_str.parse::<u32>() {
|
||||
if value > 0 {
|
||||
settings.auto_sync_interval_minutes = value;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
} else {
|
||||
ui.label(egui::RichText::new("❌ Disabled").color(egui::Color32::YELLOW));
|
||||
ui.label("(Configure host, flow & enable in-place sync)");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_config_location_section(ui: &mut egui::Ui) {
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("🗁 Config file:");
|
||||
let config_path = get_config_path();
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 20.0],
|
||||
egui::TextEdit::singleline(&mut config_path.display().to_string())
|
||||
.interactive(false)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_bottom_area(
|
||||
ui: &mut egui::Ui,
|
||||
ctx: &egui::Context,
|
||||
settings: &KhmSettings,
|
||||
connection_tab: &mut ConnectionTab,
|
||||
operation_log: &mut Vec<String>
|
||||
) {
|
||||
let button_area_height = 120.0;
|
||||
|
||||
ui.allocate_ui_with_layout(
|
||||
[ui.available_width(), button_area_height].into(),
|
||||
egui::Layout::bottom_up(egui::Align::Min),
|
||||
|ui| {
|
||||
// Operation log area
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("📄 Operation Log").size(14.0).strong());
|
||||
ui.add_space(5.0);
|
||||
|
||||
let log_text = operation_log.join("\n");
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 60.0],
|
||||
egui::TextEdit::multiline(&mut log_text.clone())
|
||||
.font(egui::FontId::monospace(10.0))
|
||||
.interactive(false)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Show validation hints
|
||||
let save_enabled = !settings.host.is_empty() && !settings.flow.is_empty();
|
||||
if !save_enabled {
|
||||
ui.label(egui::RichText::new("❗ Please fill in Host URL and Flow Name to save settings")
|
||||
.color(egui::Color32::YELLOW)
|
||||
.italics());
|
||||
}
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
// Action buttons
|
||||
render_action_buttons(ui, ctx, settings, connection_tab, save_enabled, operation_log);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn render_action_buttons(
|
||||
ui: &mut egui::Ui,
|
||||
ctx: &egui::Context,
|
||||
settings: &KhmSettings,
|
||||
connection_tab: &mut ConnectionTab,
|
||||
save_enabled: bool,
|
||||
operation_log: &mut Vec<String>
|
||||
) {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(
|
||||
save_enabled,
|
||||
egui::Button::new("💾 Save Settings")
|
||||
.min_size(egui::vec2(120.0, 32.0))
|
||||
).clicked() {
|
||||
match save_settings_validated(settings) {
|
||||
Ok(()) => {
|
||||
add_log_entry(operation_log, "✅ Settings saved successfully".to_string());
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
Err(e) => {
|
||||
add_log_entry(operation_log, format!("❌ Failed to save settings: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ui.add(
|
||||
egui::Button::new("✖ Cancel")
|
||||
.min_size(egui::vec2(80.0, 32.0))
|
||||
).clicked() {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let can_test = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_testing_connection;
|
||||
let can_sync = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_syncing;
|
||||
|
||||
if ui.add_enabled(
|
||||
can_test,
|
||||
egui::Button::new(
|
||||
if connection_tab.is_testing_connection {
|
||||
"▶ Testing..."
|
||||
} else {
|
||||
"🔍 Test Connection"
|
||||
}
|
||||
).min_size(egui::vec2(120.0, 32.0))
|
||||
).clicked() {
|
||||
add_log_entry(operation_log, "🔍 Starting connection test...".to_string());
|
||||
connection_tab.start_test(settings, ctx);
|
||||
}
|
||||
|
||||
if ui.add_enabled(
|
||||
can_sync,
|
||||
egui::Button::new(
|
||||
if connection_tab.is_syncing {
|
||||
"🔄 Syncing..."
|
||||
} else {
|
||||
"🔄 Sync Now"
|
||||
}
|
||||
).min_size(egui::vec2(100.0, 32.0))
|
||||
).clicked() {
|
||||
add_log_entry(operation_log, "🔄 Starting manual sync...".to_string());
|
||||
connection_tab.start_sync(settings, ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Add entry to operation log with timestamp
|
||||
pub fn add_log_entry(operation_log: &mut Vec<String>, message: String) {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
let secs = now.as_secs();
|
||||
let millis = now.subsec_millis();
|
||||
|
||||
// Format as HH:MM:SS.mmm
|
||||
let hours = (secs / 3600) % 24;
|
||||
let minutes = (secs / 60) % 60;
|
||||
let seconds = secs % 60;
|
||||
let timestamp = format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis);
|
||||
|
||||
let log_entry = format!("{} {}", timestamp, message);
|
||||
|
||||
operation_log.push(log_entry);
|
||||
|
||||
// Keep only last 20 entries to prevent memory growth
|
||||
if operation_log.len() > 20 {
|
||||
operation_log.remove(0);
|
||||
}
|
||||
}
|
414
src/gui/settings/window.rs
Normal file
414
src/gui/settings/window.rs
Normal file
@@ -0,0 +1,414 @@
|
||||
use eframe::egui;
|
||||
use log::info;
|
||||
use std::sync::mpsc;
|
||||
use crate::gui::common::{load_settings, KhmSettings};
|
||||
use crate::gui::admin::{AdminState, AdminOperation, render_statistics, render_search_controls,
|
||||
render_bulk_actions, render_keys_table, KeyAction, BulkAction};
|
||||
use crate::gui::api::{SshKey, bulk_deprecate_servers, bulk_restore_servers,
|
||||
deprecate_key, restore_key, delete_key};
|
||||
|
||||
use super::connection::{ConnectionTab, SettingsTab};
|
||||
use super::ui::{render_connection_tab, add_log_entry};
|
||||
|
||||
pub struct SettingsWindow {
|
||||
settings: KhmSettings,
|
||||
auto_sync_interval_str: String,
|
||||
current_tab: SettingsTab,
|
||||
connection_tab: ConnectionTab,
|
||||
admin_state: AdminState,
|
||||
admin_receiver: Option<mpsc::Receiver<Result<Vec<SshKey>, String>>>,
|
||||
operation_receiver: Option<mpsc::Receiver<Result<String, String>>>,
|
||||
operation_log: Vec<String>,
|
||||
}
|
||||
|
||||
impl SettingsWindow {
|
||||
pub fn new() -> Self {
|
||||
let settings = load_settings();
|
||||
let auto_sync_interval_str = settings.auto_sync_interval_minutes.to_string();
|
||||
|
||||
Self {
|
||||
settings,
|
||||
auto_sync_interval_str,
|
||||
current_tab: SettingsTab::Connection,
|
||||
connection_tab: ConnectionTab::default(),
|
||||
admin_state: AdminState::default(),
|
||||
admin_receiver: None,
|
||||
operation_receiver: None,
|
||||
operation_log: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for SettingsWindow {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
// Check for admin operation results
|
||||
self.check_admin_results(ctx);
|
||||
|
||||
// Apply enhanced modern dark theme
|
||||
apply_modern_theme(ctx);
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::none().inner_margin(egui::Margin::same(20.0)))
|
||||
.show(ctx, |ui| {
|
||||
// Header with title
|
||||
ui.horizontal(|ui| {
|
||||
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, "📃 Settings");
|
||||
ui.selectable_value(&mut self.current_tab, SettingsTab::Admin, "🔧 Admin");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.add_space(15.0);
|
||||
|
||||
match self.current_tab {
|
||||
SettingsTab::Connection => {
|
||||
render_connection_tab(
|
||||
ui,
|
||||
ctx,
|
||||
&mut self.settings,
|
||||
&mut self.auto_sync_interval_str,
|
||||
&mut self.connection_tab,
|
||||
&mut self.operation_log
|
||||
);
|
||||
}
|
||||
SettingsTab::Admin => {
|
||||
self.render_admin_tab(ui, ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsWindow {
|
||||
fn check_admin_results(&mut self, ctx: &egui::Context) {
|
||||
// Check for admin keys loading result
|
||||
if let Some(receiver) = &self.admin_receiver {
|
||||
if let Ok(result) = receiver.try_recv() {
|
||||
self.admin_state.handle_keys_loaded(result);
|
||||
self.admin_receiver = None;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for operation results
|
||||
if let Some(receiver) = &self.operation_receiver {
|
||||
if let Ok(result) = receiver.try_recv() {
|
||||
match result {
|
||||
Ok(message) => {
|
||||
info!("Operation completed: {}", message);
|
||||
add_log_entry(&mut self.operation_log, format!("✅ {}", message));
|
||||
// Reload keys after operation
|
||||
self.load_admin_keys(ctx);
|
||||
}
|
||||
Err(error) => {
|
||||
add_log_entry(&mut self.operation_log, format!("❌ Operation failed: {}", error));
|
||||
}
|
||||
}
|
||||
self.admin_state.current_operation = AdminOperation::None;
|
||||
self.operation_receiver = None;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_admin_tab(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
|
||||
// Admin tab header
|
||||
ui.horizontal(|ui| {
|
||||
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() {
|
||||
self.load_admin_keys(ctx);
|
||||
}
|
||||
|
||||
if let Some(last_load) = self.admin_state.last_load_time {
|
||||
let elapsed = last_load.elapsed().as_secs();
|
||||
ui.label(format!("Updated {}s ago", elapsed));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.add_space(10.0);
|
||||
|
||||
// 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")
|
||||
.size(16.0)
|
||||
.color(egui::Color32::YELLOW));
|
||||
ui.add_space(10.0);
|
||||
if ui.button("Go to Connection Settings").clicked() {
|
||||
self.current_tab = SettingsTab::Connection;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Load keys automatically on first view
|
||||
if self.admin_state.keys.is_empty() && !matches!(self.admin_state.current_operation, AdminOperation::LoadingKeys) {
|
||||
self.load_admin_keys(ctx);
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
if matches!(self.admin_state.current_operation, AdminOperation::LoadingKeys) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.spinner();
|
||||
ui.label("Loading keys...");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Statistics section
|
||||
render_statistics(ui, &self.admin_state);
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Search and filters
|
||||
render_search_controls(ui, &mut self.admin_state);
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Bulk actions
|
||||
let bulk_action = render_bulk_actions(ui, &mut self.admin_state);
|
||||
self.handle_bulk_action(bulk_action, ctx);
|
||||
|
||||
if self.admin_state.selected_servers.values().any(|&v| v) {
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
// Keys table
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(450.0)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
let key_action = render_keys_table(ui, &mut self.admin_state);
|
||||
self.handle_key_action(key_action, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
fn load_admin_keys(&mut self, ctx: &egui::Context) {
|
||||
if let Some(receiver) = self.admin_state.load_keys(&self.settings, ctx) {
|
||||
self.admin_receiver = Some(receiver);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_bulk_action(&mut self, action: BulkAction, ctx: &egui::Context) {
|
||||
match action {
|
||||
BulkAction::DeprecateSelected => {
|
||||
let selected = self.admin_state.get_selected_servers();
|
||||
if !selected.is_empty() {
|
||||
self.start_bulk_deprecate(selected, ctx);
|
||||
}
|
||||
}
|
||||
BulkAction::RestoreSelected => {
|
||||
let selected = self.admin_state.get_selected_servers();
|
||||
if !selected.is_empty() {
|
||||
self.start_bulk_restore(selected, ctx);
|
||||
}
|
||||
}
|
||||
BulkAction::ClearSelection => {
|
||||
// Selection already cleared in UI
|
||||
}
|
||||
BulkAction::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key_action(&mut self, action: KeyAction, ctx: &egui::Context) {
|
||||
match action {
|
||||
KeyAction::DeprecateKey(server) | KeyAction::DeprecateServer(server) => {
|
||||
self.start_deprecate_key(&server, ctx);
|
||||
}
|
||||
KeyAction::RestoreKey(server) | KeyAction::RestoreServer(server) => {
|
||||
self.start_restore_key(&server, ctx);
|
||||
}
|
||||
KeyAction::DeleteKey(server) => {
|
||||
self.start_delete_key(&server, ctx);
|
||||
}
|
||||
KeyAction::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_bulk_deprecate(&mut self, servers: Vec<String>, ctx: &egui::Context) {
|
||||
self.admin_state.current_operation = AdminOperation::BulkDeprecating;
|
||||
add_log_entry(&mut self.operation_log, format!("Deprecating {} servers...", servers.len()));
|
||||
|
||||
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, servers).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
fn start_bulk_restore(&mut self, servers: Vec<String>, ctx: &egui::Context) {
|
||||
self.admin_state.current_operation = AdminOperation::BulkRestoring;
|
||||
add_log_entry(&mut self.operation_log, format!("Restoring {} servers...", servers.len()));
|
||||
|
||||
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, servers).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
fn start_deprecate_key(&mut self, server: &str, ctx: &egui::Context) {
|
||||
self.admin_state.current_operation = AdminOperation::DeprecatingKey;
|
||||
add_log_entry(&mut self.operation_log, format!("Deprecating key for server: {}", server));
|
||||
|
||||
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(host, flow, basic_auth, server_name).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
fn start_restore_key(&mut self, server: &str, ctx: &egui::Context) {
|
||||
self.admin_state.current_operation = AdminOperation::RestoringKey;
|
||||
add_log_entry(&mut self.operation_log, format!("Restoring key for server: {}", server));
|
||||
|
||||
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(host, flow, basic_auth, server_name).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
fn start_delete_key(&mut self, server: &str, ctx: &egui::Context) {
|
||||
self.admin_state.current_operation = AdminOperation::DeletingKey;
|
||||
add_log_entry(&mut self.operation_log, format!("Deleting key for server: {}", server));
|
||||
|
||||
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 {
|
||||
delete_key(host, flow, basic_auth, server_name).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.request_repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply modern dark theme for the settings window
|
||||
fn apply_modern_theme(ctx: &egui::Context) {
|
||||
let mut visuals = egui::Visuals::dark();
|
||||
visuals.window_fill = egui::Color32::from_gray(25);
|
||||
visuals.panel_fill = egui::Color32::from_gray(30);
|
||||
visuals.faint_bg_color = egui::Color32::from_gray(35);
|
||||
visuals.extreme_bg_color = egui::Color32::from_gray(15);
|
||||
visuals.button_frame = true;
|
||||
visuals.collapsing_header_frame = true;
|
||||
visuals.indent_has_left_vline = true;
|
||||
visuals.menu_rounding = egui::Rounding::same(8.0);
|
||||
visuals.window_rounding = egui::Rounding::same(12.0);
|
||||
visuals.widgets.noninteractive.rounding = egui::Rounding::same(6.0);
|
||||
visuals.widgets.inactive.rounding = egui::Rounding::same(6.0);
|
||||
visuals.widgets.hovered.rounding = egui::Rounding::same(6.0);
|
||||
visuals.widgets.active.rounding = egui::Rounding::same(6.0);
|
||||
ctx.set_visuals(visuals);
|
||||
}
|
||||
|
||||
/// Create window icon for settings window
|
||||
pub fn create_window_icon() -> egui::IconData {
|
||||
// Create a simple programmatic icon (blue square with white border)
|
||||
let icon_size = 32;
|
||||
let icon_data: Vec<u8> = (0..icon_size * icon_size)
|
||||
.flat_map(|i| {
|
||||
let y = i / icon_size;
|
||||
let x = i % icon_size;
|
||||
if x < 2 || x >= 30 || y < 2 || y >= 30 {
|
||||
[255, 255, 255, 255] // White border
|
||||
} else {
|
||||
[64, 128, 255, 255] // Blue center
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
egui::IconData {
|
||||
rgba: icon_data,
|
||||
width: icon_size as u32,
|
||||
height: icon_size as u32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the settings window application
|
||||
pub fn run_settings_window() {
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("KHM Settings")
|
||||
.with_inner_size([600.0, 800.0])
|
||||
.with_min_inner_size([500.0, 650.0])
|
||||
.with_resizable(true)
|
||||
.with_icon(create_window_icon()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = eframe::run_native(
|
||||
"KHM Settings",
|
||||
options,
|
||||
Box::new(|_cc| Ok(Box::new(SettingsWindow::new()))),
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user