mirror of
https://github.com/house-of-vanity/khm.git
synced 2025-08-21 14:27:14 +00:00
Added egui admin
This commit is contained in:
@@ -2,9 +2,11 @@ use dirs::home_dir;
|
||||
use eframe::egui;
|
||||
use log::{debug, error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
use reqwest::Client;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KhmSettings {
|
||||
@@ -16,6 +18,94 @@ pub struct KhmSettings {
|
||||
pub auto_sync_interval_minutes: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SshKey {
|
||||
pub server: String,
|
||||
pub public_key: String,
|
||||
#[serde(default)]
|
||||
pub deprecated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum AdminOperation {
|
||||
LoadingKeys,
|
||||
DeprecatingKey(String),
|
||||
RestoringKey(String),
|
||||
DeletingKey(String),
|
||||
BulkDeprecating(Vec<String>),
|
||||
BulkRestoring(Vec<String>),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AdminState {
|
||||
keys: Vec<SshKey>,
|
||||
filtered_keys: Vec<SshKey>,
|
||||
search_term: String,
|
||||
show_deprecated_only: bool,
|
||||
selected_servers: HashMap<String, bool>,
|
||||
expanded_servers: HashMap<String, bool>,
|
||||
current_operation: AdminOperation,
|
||||
last_load_time: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
impl Default for AdminState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
keys: Vec::new(),
|
||||
filtered_keys: Vec::new(),
|
||||
search_term: String::new(),
|
||||
show_deprecated_only: false,
|
||||
selected_servers: HashMap::new(),
|
||||
expanded_servers: HashMap::new(),
|
||||
current_operation: AdminOperation::None,
|
||||
last_load_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_admin_keys(host: String, flow: String, basic_auth: String) -> Result<Vec<SshKey>, String> {
|
||||
if host.is_empty() || flow.is_empty() {
|
||||
return Err("Host and flow must be specified".to_string());
|
||||
}
|
||||
|
||||
let url = format!("{}/{}/keys?include_deprecated=true", host.trim_end_matches('/'), flow);
|
||||
info!("Fetching admin keys from: {}", 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.get(&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))?;
|
||||
|
||||
let keys: Vec<SshKey> = serde_json::from_str(&body)
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
info!("Fetched {} SSH keys", keys.len());
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
impl Default for KhmSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -75,6 +165,16 @@ struct KhmSettingsWindow {
|
||||
connection_status: ConnectionStatus,
|
||||
is_testing_connection: bool,
|
||||
test_result_receiver: Option<mpsc::Receiver<Result<String, String>>>,
|
||||
admin_state: AdminState,
|
||||
admin_receiver: Option<mpsc::Receiver<Result<Vec<SshKey>, String>>>,
|
||||
operation_receiver: Option<mpsc::Receiver<Result<String, String>>>,
|
||||
current_tab: SettingsTab,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum SettingsTab {
|
||||
Connection,
|
||||
Admin,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -112,13 +212,63 @@ impl eframe::App for KhmSettingsWindow {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
// Apply modern dark theme
|
||||
ctx.set_visuals(egui::Visuals {
|
||||
button_frame: true,
|
||||
collapsing_header_frame: true,
|
||||
indent_has_left_vline: true,
|
||||
..egui::Visuals::dark()
|
||||
});
|
||||
|
||||
// Check for admin operation results
|
||||
if let Some(receiver) = &self.admin_receiver {
|
||||
if let Ok(result) = receiver.try_recv() {
|
||||
match result {
|
||||
Ok(keys) => {
|
||||
self.admin_state.keys = keys;
|
||||
self.admin_state.last_load_time = Some(std::time::Instant::now());
|
||||
self.filter_admin_keys();
|
||||
self.admin_state.current_operation = AdminOperation::None;
|
||||
info!("Keys loaded successfully: {} keys", self.admin_state.keys.len());
|
||||
}
|
||||
Err(error) => {
|
||||
self.admin_state.current_operation = AdminOperation::None;
|
||||
error!("Failed to load keys: {}", error);
|
||||
}
|
||||
}
|
||||
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);
|
||||
// Reload keys after operation
|
||||
self.load_admin_keys(ctx);
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Operation failed: {}", error);
|
||||
}
|
||||
}
|
||||
self.admin_state.current_operation = AdminOperation::None;
|
||||
self.operation_receiver = None;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
// Apply enhanced modern dark theme for admin interface
|
||||
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);
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::none().inner_margin(egui::Margin::same(20.0)))
|
||||
@@ -129,9 +279,26 @@ impl eframe::App for KhmSettingsWindow {
|
||||
});
|
||||
|
||||
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::Admin, "🛠️ Admin");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.add_space(15.0);
|
||||
|
||||
match self.current_tab {
|
||||
SettingsTab::Connection => self.render_connection_tab(ui, ctx),
|
||||
SettingsTab::Admin => self.render_admin_tab(ui, ctx),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl KhmSettingsWindow {
|
||||
fn render_connection_tab(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
|
||||
// Connection section
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
@@ -347,7 +514,568 @@ impl eframe::App for KhmSettingsWindow {
|
||||
.color(egui::Color32::YELLOW)
|
||||
.italics());
|
||||
}
|
||||
}
|
||||
|
||||
fn start_connection_test(&mut self, 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 = 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 {
|
||||
test_khm_connection(host, flow, basic_auth).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.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 cards
|
||||
ui.horizontal(|ui| {
|
||||
let total_keys = self.admin_state.keys.len();
|
||||
let active_keys = self.admin_state.keys.iter().filter(|k| !k.deprecated).count();
|
||||
let deprecated_keys = total_keys - active_keys;
|
||||
let unique_servers = self.admin_state.keys.iter().map(|k| &k.server).collect::<std::collections::HashSet<_>>().len();
|
||||
|
||||
// Total keys card
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(80.0);
|
||||
ui.vertical_centered(|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 card
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(80.0);
|
||||
ui.vertical_centered(|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 card
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(80.0);
|
||||
ui.vertical_centered(|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 card
|
||||
ui.group(|ui| {
|
||||
ui.set_min_width(80.0);
|
||||
ui.vertical_centered(|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.add_space(10.0);
|
||||
|
||||
// Enhanced search and filters
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("🔍").size(16.0));
|
||||
let search_response = ui.add_sized(
|
||||
[200.0, 24.0],
|
||||
egui::TextEdit::singleline(&mut self.admin_state.search_term)
|
||||
.hint_text("Search servers or keys...")
|
||||
);
|
||||
|
||||
if self.admin_state.search_term.is_empty() {
|
||||
ui.label(egui::RichText::new("Type to search").color(egui::Color32::GRAY));
|
||||
} else {
|
||||
ui.label(format!("{} results", self.admin_state.filtered_keys.len()));
|
||||
if ui.small_button("✕").clicked() {
|
||||
self.admin_state.search_term.clear();
|
||||
self.filter_admin_keys();
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Filter toggle with better styling
|
||||
let show_deprecated = self.admin_state.show_deprecated_only;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Filter:");
|
||||
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() {
|
||||
self.admin_state.show_deprecated_only = true;
|
||||
self.filter_admin_keys();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle search text changes
|
||||
if search_response.changed() {
|
||||
self.filter_admin_keys();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Enhanced bulk actions
|
||||
let selected_count = self.admin_state.selected_servers.values().filter(|&&v| v).count();
|
||||
if selected_count > 0 {
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("📋").size(16.0));
|
||||
ui.label(egui::RichText::new(format!("Selected {} servers", selected_count))
|
||||
.size(14.0)
|
||||
.strong()
|
||||
.color(egui::Color32::LIGHT_BLUE));
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui.add(egui::Button::new("⚠ Deprecate Selected")
|
||||
.fill(egui::Color32::from_rgb(230, 126, 34))
|
||||
.min_size(egui::vec2(130.0, 30.0))
|
||||
).clicked() {
|
||||
self.deprecate_selected_servers(ctx);
|
||||
}
|
||||
|
||||
if ui.add(egui::Button::new("✓ Restore Selected")
|
||||
.fill(egui::Color32::from_rgb(46, 204, 113))
|
||||
.min_size(egui::vec2(120.0, 30.0))
|
||||
).clicked() {
|
||||
self.restore_selected_servers(ctx);
|
||||
}
|
||||
|
||||
if ui.add(egui::Button::new("✕ Clear Selection")
|
||||
.fill(egui::Color32::from_rgb(149, 165, 166))
|
||||
.min_size(egui::vec2(110.0, 30.0))
|
||||
).clicked() {
|
||||
self.admin_state.selected_servers.clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
// Modern scrollable keys table with better styling
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(450.0)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
if self.admin_state.filtered_keys.is_empty() && !self.admin_state.search_term.is_empty() {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(40.0);
|
||||
ui.label(egui::RichText::new("🔍").size(48.0).color(egui::Color32::GRAY));
|
||||
ui.label(egui::RichText::new("No results found")
|
||||
.size(18.0)
|
||||
.color(egui::Color32::GRAY));
|
||||
ui.label(egui::RichText::new(format!("Try adjusting your search: '{}'", self.admin_state.search_term))
|
||||
.size(14.0)
|
||||
.color(egui::Color32::DARK_GRAY));
|
||||
});
|
||||
} else {
|
||||
self.render_keys_table(ui, ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn load_admin_keys(&mut self, ctx: &egui::Context) {
|
||||
if self.settings.host.is_empty() || self.settings.flow.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.admin_state.current_operation = AdminOperation::LoadingKeys;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.admin_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 {
|
||||
fetch_admin_keys(host, flow, basic_auth).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
fn filter_admin_keys(&mut self) {
|
||||
let mut filtered = self.admin_state.keys.clone();
|
||||
|
||||
// Apply deprecated filter
|
||||
if self.admin_state.show_deprecated_only {
|
||||
filtered.retain(|key| key.deprecated);
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if !self.admin_state.search_term.is_empty() {
|
||||
let search_term = self.admin_state.search_term.to_lowercase();
|
||||
filtered.retain(|key| {
|
||||
key.server.to_lowercase().contains(&search_term) ||
|
||||
key.public_key.to_lowercase().contains(&search_term)
|
||||
});
|
||||
}
|
||||
|
||||
self.admin_state.filtered_keys = filtered;
|
||||
}
|
||||
|
||||
fn render_keys_table(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
|
||||
if self.admin_state.filtered_keys.is_empty() {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(60.0);
|
||||
if self.admin_state.keys.is_empty() {
|
||||
ui.label(egui::RichText::new("🔑").size(48.0).color(egui::Color32::GRAY));
|
||||
ui.label(egui::RichText::new("No SSH keys available")
|
||||
.size(18.0)
|
||||
.color(egui::Color32::GRAY));
|
||||
ui.label(egui::RichText::new("Keys will appear here once loaded from the server")
|
||||
.size(14.0)
|
||||
.color(egui::Color32::DARK_GRAY));
|
||||
} else {
|
||||
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));
|
||||
ui.label(egui::RichText::new("Try adjusting your search or filter settings")
|
||||
.size(14.0)
|
||||
.color(egui::Color32::DARK_GRAY));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Group keys by server - clone to avoid borrowing conflicts
|
||||
let filtered_keys = self.admin_state.filtered_keys.clone();
|
||||
let expanded_servers = self.admin_state.expanded_servers.clone();
|
||||
let selected_servers = self.admin_state.selected_servers.clone();
|
||||
|
||||
let mut servers: std::collections::BTreeMap<String, Vec<SshKey>> = std::collections::BTreeMap::new();
|
||||
for key in &filtered_keys {
|
||||
servers.entry(key.server.clone()).or_insert_with(Vec::new).push(key.clone());
|
||||
}
|
||||
|
||||
// Render each server group
|
||||
for (server_name, server_keys) in servers {
|
||||
let is_expanded = expanded_servers.get(&server_name).copied().unwrap_or(false);
|
||||
let active_count = server_keys.iter().filter(|k| !k.deprecated).count();
|
||||
let deprecated_count = server_keys.len() - active_count;
|
||||
|
||||
// Modern server header with enhanced styling
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// Stylized checkbox for server selection
|
||||
let mut selected = selected_servers.get(&server_name).copied().unwrap_or(false);
|
||||
if ui.add(egui::Checkbox::new(&mut selected, "")
|
||||
.indeterminate(false)
|
||||
).changed() {
|
||||
self.admin_state.selected_servers.insert(server_name.clone(), selected);
|
||||
}
|
||||
|
||||
// Modern expand/collapse button
|
||||
let expand_icon = if is_expanded { "▼" } else { "▶" };
|
||||
if ui.add(egui::Button::new(expand_icon)
|
||||
.fill(egui::Color32::TRANSPARENT)
|
||||
.stroke(egui::Stroke::NONE)
|
||||
.min_size(egui::vec2(20.0, 20.0))
|
||||
).clicked() {
|
||||
self.admin_state.expanded_servers.insert(server_name.clone(), !is_expanded);
|
||||
}
|
||||
|
||||
// Server icon and name
|
||||
ui.label(egui::RichText::new("🖥").size(16.0));
|
||||
ui.label(egui::RichText::new(&server_name)
|
||||
.size(15.0)
|
||||
.strong()
|
||||
.color(egui::Color32::WHITE));
|
||||
|
||||
// Keys count badge
|
||||
let (rect, _) = ui.allocate_exact_size(
|
||||
egui::vec2(60.0, 20.0),
|
||||
egui::Sense::hover()
|
||||
);
|
||||
ui.painter().rect_filled(
|
||||
rect,
|
||||
egui::Rounding::same(10.0),
|
||||
egui::Color32::from_rgb(52, 152, 219)
|
||||
);
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
&format!("{} keys", server_keys.len()),
|
||||
egui::FontId::proportional(11.0),
|
||||
egui::Color32::WHITE,
|
||||
);
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Status indicators
|
||||
if deprecated_count > 0 {
|
||||
let (rect, _) = ui.allocate_exact_size(
|
||||
egui::vec2(80.0, 20.0),
|
||||
egui::Sense::hover()
|
||||
);
|
||||
ui.painter().rect_filled(
|
||||
rect,
|
||||
egui::Rounding::same(10.0),
|
||||
egui::Color32::from_rgb(231, 76, 60)
|
||||
);
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
&format!("{} deprecated", deprecated_count),
|
||||
egui::FontId::proportional(10.0),
|
||||
egui::Color32::WHITE,
|
||||
);
|
||||
}
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Stylized action buttons
|
||||
if deprecated_count > 0 {
|
||||
if ui.add(egui::Button::new("✓ Restore All")
|
||||
.fill(egui::Color32::from_rgb(46, 204, 113))
|
||||
.min_size(egui::vec2(90.0, 28.0))
|
||||
).clicked() {
|
||||
self.restore_server_keys(&server_name, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
if active_count > 0 {
|
||||
if ui.add(egui::Button::new("⚠ Deprecate All")
|
||||
.fill(egui::Color32::from_rgb(230, 126, 34))
|
||||
.min_size(egui::vec2(110.0, 28.0))
|
||||
).clicked() {
|
||||
self.deprecate_server_keys(&server_name, ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Expanded key details
|
||||
if is_expanded {
|
||||
ui.indent("server_keys", |ui| {
|
||||
for key in &server_keys {
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// Key type badge with modern styling
|
||||
let key_type = self.get_key_type(&key.public_key);
|
||||
let (badge_color, text_color) = match key_type.as_str() {
|
||||
"RSA" => (egui::Color32::from_rgb(52, 144, 220), egui::Color32::WHITE),
|
||||
"ED25519" => (egui::Color32::from_rgb(46, 204, 113), egui::Color32::WHITE),
|
||||
"ECDSA" => (egui::Color32::from_rgb(241, 196, 15), egui::Color32::BLACK),
|
||||
"DSA" => (egui::Color32::from_rgb(230, 126, 34), egui::Color32::WHITE),
|
||||
_ => (egui::Color32::GRAY, egui::Color32::WHITE),
|
||||
};
|
||||
|
||||
// Custom badge rendering
|
||||
let (rect, _) = ui.allocate_exact_size(
|
||||
egui::vec2(50.0, 20.0),
|
||||
egui::Sense::hover()
|
||||
);
|
||||
ui.painter().rect_filled(
|
||||
rect,
|
||||
egui::Rounding::same(4.0),
|
||||
badge_color
|
||||
);
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
&key_type,
|
||||
egui::FontId::proportional(11.0),
|
||||
text_color,
|
||||
);
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Status badge with icons
|
||||
if key.deprecated {
|
||||
ui.label(egui::RichText::new("⚠ DEPRECATED")
|
||||
.size(12.0)
|
||||
.color(egui::Color32::from_rgb(231, 76, 60))
|
||||
.strong());
|
||||
} else {
|
||||
ui.label(egui::RichText::new("✓ ACTIVE")
|
||||
.size(12.0)
|
||||
.color(egui::Color32::from_rgb(46, 204, 113))
|
||||
.strong());
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Key preview with monospace font
|
||||
ui.label(egui::RichText::new(self.get_key_preview(&key.public_key))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::LIGHT_GRAY));
|
||||
|
||||
let server_name_for_action = server_name.clone();
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Modern action buttons
|
||||
if key.deprecated {
|
||||
if ui.add(egui::Button::new("✓ Restore")
|
||||
.fill(egui::Color32::from_rgb(46, 204, 113))
|
||||
.min_size(egui::vec2(70.0, 24.0))
|
||||
).clicked() {
|
||||
self.restore_key(&server_name_for_action, ctx);
|
||||
}
|
||||
if ui.add(egui::Button::new("🗑 Delete")
|
||||
.fill(egui::Color32::from_rgb(231, 76, 60))
|
||||
.min_size(egui::vec2(70.0, 24.0))
|
||||
).clicked() {
|
||||
self.delete_key(&server_name_for_action, ctx);
|
||||
}
|
||||
} else {
|
||||
if ui.add(egui::Button::new("⚠ Deprecate")
|
||||
.fill(egui::Color32::from_rgb(230, 126, 34))
|
||||
.min_size(egui::vec2(85.0, 24.0))
|
||||
).clicked() {
|
||||
self.deprecate_key(&server_name_for_action, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
if ui.add(egui::Button::new("📋 Copy")
|
||||
.fill(egui::Color32::from_rgb(52, 152, 219))
|
||||
.min_size(egui::vec2(60.0, 24.0))
|
||||
).clicked() {
|
||||
ui.output_mut(|o| o.copied_text = key.public_key.clone());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(5.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn deprecate_selected_servers(&mut self, _ctx: &egui::Context) {
|
||||
// Stub for now
|
||||
}
|
||||
|
||||
fn restore_selected_servers(&mut self, _ctx: &egui::Context) {
|
||||
// Stub for now
|
||||
}
|
||||
|
||||
fn get_key_type(&self, public_key: &str) -> String {
|
||||
if public_key.starts_with("ssh-rsa") {
|
||||
"RSA".to_string()
|
||||
} else if public_key.starts_with("ssh-ed25519") {
|
||||
"ED25519".to_string()
|
||||
} else if public_key.starts_with("ecdsa-sha2-nistp") {
|
||||
"ECDSA".to_string()
|
||||
} else if public_key.starts_with("ssh-dss") {
|
||||
"DSA".to_string()
|
||||
} else {
|
||||
"Unknown".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key_preview(&self, public_key: &str) -> String {
|
||||
let parts: Vec<&str> = public_key.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let key_part = parts[1];
|
||||
if key_part.len() > 20 {
|
||||
format!("{}...", &key_part[..20])
|
||||
} else {
|
||||
key_part.to_string()
|
||||
}
|
||||
} else {
|
||||
format!("{}...", &public_key[..std::cmp::min(20, public_key.len())])
|
||||
}
|
||||
}
|
||||
|
||||
fn deprecate_server_keys(&mut self, _server: &str, _ctx: &egui::Context) {
|
||||
// Stub for now
|
||||
}
|
||||
|
||||
fn restore_server_keys(&mut self, _server: &str, _ctx: &egui::Context) {
|
||||
// Stub for now
|
||||
}
|
||||
|
||||
fn deprecate_key(&mut self, _server: &str, _ctx: &egui::Context) {
|
||||
// Stub for now
|
||||
}
|
||||
|
||||
fn restore_key(&mut self, _server: &str, _ctx: &egui::Context) {
|
||||
// Stub for now
|
||||
}
|
||||
|
||||
fn delete_key(&mut self, _server: &str, _ctx: &egui::Context) {
|
||||
// Stub for now
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +1109,10 @@ pub fn run_settings_window() {
|
||||
connection_status: ConnectionStatus::Unknown,
|
||||
is_testing_connection: false,
|
||||
test_result_receiver: None,
|
||||
admin_state: AdminState::default(),
|
||||
admin_receiver: None,
|
||||
operation_receiver: None,
|
||||
current_tab: SettingsTab::Connection,
|
||||
}))),
|
||||
);
|
||||
}
|
||||
@@ -407,35 +1139,6 @@ fn create_window_icon() -> egui::IconData {
|
||||
}
|
||||
}
|
||||
|
||||
impl KhmSettingsWindow {
|
||||
fn start_connection_test(&mut self, 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 = 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 {
|
||||
test_khm_connection(host, flow, basic_auth).await
|
||||
});
|
||||
|
||||
let _ = tx.send(result);
|
||||
ctx_clone.request_repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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