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 eframe::egui;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct KhmSettings {
|
pub struct KhmSettings {
|
||||||
@@ -16,6 +18,94 @@ pub struct KhmSettings {
|
|||||||
pub auto_sync_interval_minutes: u32,
|
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 {
|
impl Default for KhmSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -75,6 +165,16 @@ struct KhmSettingsWindow {
|
|||||||
connection_status: ConnectionStatus,
|
connection_status: ConnectionStatus,
|
||||||
is_testing_connection: bool,
|
is_testing_connection: bool,
|
||||||
test_result_receiver: Option<mpsc::Receiver<Result<String, String>>>,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -112,13 +212,63 @@ impl eframe::App for KhmSettingsWindow {
|
|||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Apply modern dark theme
|
|
||||||
ctx.set_visuals(egui::Visuals {
|
// Check for admin operation results
|
||||||
button_frame: true,
|
if let Some(receiver) = &self.admin_receiver {
|
||||||
collapsing_header_frame: true,
|
if let Ok(result) = receiver.try_recv() {
|
||||||
indent_has_left_vline: true,
|
match result {
|
||||||
..egui::Visuals::dark()
|
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()
|
egui::CentralPanel::default()
|
||||||
.frame(egui::Frame::none().inner_margin(egui::Margin::same(20.0)))
|
.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);
|
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.separator();
|
||||||
ui.add_space(15.0);
|
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
|
// Connection section
|
||||||
ui.group(|ui| {
|
ui.group(|ui| {
|
||||||
ui.set_min_width(ui.available_width());
|
ui.set_min_width(ui.available_width());
|
||||||
@@ -347,7 +514,568 @@ impl eframe::App for KhmSettingsWindow {
|
|||||||
.color(egui::Color32::YELLOW)
|
.color(egui::Color32::YELLOW)
|
||||||
.italics());
|
.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,
|
connection_status: ConnectionStatus::Unknown,
|
||||||
is_testing_connection: false,
|
is_testing_connection: false,
|
||||||
test_result_receiver: None,
|
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> {
|
async fn test_khm_connection(host: String, flow: String, basic_auth: String) -> Result<String, String> {
|
||||||
if host.is_empty() || flow.is_empty() {
|
if host.is_empty() || flow.is_empty() {
|
||||||
return Err("Host and flow must be specified".to_string());
|
return Err("Host and flow must be specified".to_string());
|
||||||
|
Reference in New Issue
Block a user