Files
I-SecretUpdate/src/ui/app_list_view.rs
T
2026-01-29 09:57:44 +01:00

215 lines
7.1 KiB
Rust

use crate::azure::models::Application;
use crate::state::AppState;
use crate::ui::components::{
show_error_message, show_loading_spinner, show_section_header, show_success_message, show_text,
};
use egui::{Context, ScrollArea, Ui};
pub fn show_app_list_view(ctx: &Context, state: &mut AppState, config: &crate::config::Config) -> Option<AppListAction> {
let mut action = None;
// Handle keyboard navigation for app list
// Check early, before text edits might consume the input
let any_text_focused = ctx.memory(|m| m.focused().is_some());
if !any_text_focused {
let filtered_apps = state.filtered_applications();
if !filtered_apps.is_empty() {
let current_idx = state.selected_app.as_ref()
.and_then(|sel| filtered_apps.iter().position(|a| a.id == sel.id));
ctx.input(|i| {
let mut target_idx = None;
if i.key_pressed(egui::Key::ArrowDown) {
target_idx = Some(current_idx.map_or(0, |idx| (idx + 1) % filtered_apps.len()));
}
if i.key_pressed(egui::Key::ArrowUp) {
target_idx = Some(current_idx.map_or(
filtered_apps.len() - 1,
|idx| if idx > 0 { idx - 1 } else { filtered_apps.len() - 1 }
));
}
if let Some(idx) = target_idx {
if let Some(app) = filtered_apps.get(idx) {
action = Some(AppListAction::SelectApp(app.clone()));
}
}
});
}
}
egui::TopBottomPanel::top("top_panel")
.frame(egui::Frame::none()
.fill(egui::Color32::TRANSPARENT)
.stroke(egui::Stroke::NONE)
.inner_margin(egui::Margin::symmetric(20.0, 10.0)))
.show_separator_line(false)
.show(ctx, |ui| {
ui.horizontal(|ui| {
show_section_header(ui, "App Registrations");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Sign Out").clicked() {
action = Some(AppListAction::SignOut);
}
ui.add_space(10.0); // Replace separator with space
if ui.button("🔄 Refresh").clicked() {
action = Some(AppListAction::Refresh);
}
});
});
if let Some(user) = &state.user_info {
show_text(ui, &format!("Logged in as: {}", user.display_name));
}
});
egui::CentralPanel::default()
.frame(egui::Frame::none()
.fill(egui::Color32::TRANSPARENT)
.inner_margin(egui::Margin {
left: 20.0,
right: 20.0,
top: 20.0,
bottom: 80.0, // Extra margin so cards don't show behind bottom panel
}))
.show(ctx, |ui| {
// Show messages
if let Some(error) = &state.error_message {
show_error_message(ui, error);
ui.add_space(10.0);
}
if let Some(success) = &state.success_message {
show_success_message(ui, success);
ui.add_space(10.0);
}
// Show loading spinner if loading
if state.operations.load_applications.is_some() {
show_loading_spinner(ui, "Loading applications...");
return;
}
// Search box
ui.horizontal(|ui| {
show_text(ui, "Search:");
ui.text_edit_singleline(&mut state.app_search_filter);
});
ui.add_space(10.0);
let filtered_apps = state.filtered_applications();
if filtered_apps.is_empty() {
ui.centered_and_justified(|ui| {
if state.applications.is_empty() {
show_text(ui, "No app registrations found. Click Refresh to load.");
} else {
show_text(ui, "No matching applications found.");
}
});
} else {
show_text(ui, &format!("Found {} applications", filtered_apps.len()));
ui.add_space(5.0);
ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| {
ui.set_width(ui.available_width() - 20.0); // Add margin for scrollbar
for app in filtered_apps {
show_app_card(ui, &app, &state.selected_app, &mut action, config);
}
});
}
});
egui::TopBottomPanel::bottom("bottom_panel")
.frame(egui::Frame::none()
.fill(egui::Color32::TRANSPARENT)
.stroke(egui::Stroke::NONE)
.inner_margin(egui::Margin::symmetric(20.0, 15.0)))
.show_separator_line(false)
.show(ctx, |ui| {
ui.horizontal(|ui| {
let is_app_selected = state.selected_app.is_some();
if ui
.add_enabled(is_app_selected, egui::Button::new("Create Secret").min_size(egui::vec2(120.0, 32.0)))
.clicked()
{
action = Some(AppListAction::CreateSecret);
}
if !is_app_selected {
show_text(ui, "Select an app registration to create a secret");
}
});
});
action
}
fn show_app_card(
ui: &mut Ui,
app: &Application,
selected_app: &Option<Application>,
action: &mut Option<AppListAction>,
config: &crate::config::Config,
) {
let is_selected = selected_app
.as_ref()
.map(|selected| selected.id == app.id)
.unwrap_or(false);
// Get card colors from config with fallback to defaults
let card_colors = config.colors.as_ref()
.map(|c| &c.cards.app_list);
let frame = if is_selected {
let bg = card_colors.map(|c| c.selected_bg).unwrap_or([60, 60, 100, 180]);
let border = card_colors.map(|c| c.selected_border).unwrap_or([100, 150, 255]);
egui::Frame::none()
.fill(egui::Color32::from_rgba_unmultiplied(bg[0], bg[1], bg[2], bg[3]))
.inner_margin(10.0)
.rounding(5.0)
.stroke(egui::Stroke::new(2.0, egui::Color32::from_rgb(border[0], border[1], border[2])))
} else {
let bg = card_colors.map(|c| c.unselected_bg).unwrap_or([40, 40, 60, 150]);
egui::Frame::none()
.fill(egui::Color32::from_rgba_unmultiplied(bg[0], bg[1], bg[2], bg[3]))
.inner_margin(10.0)
.rounding(5.0)
};
let response = frame.show(ui, |ui| {
ui.vertical(|ui| {
ui.set_width(ui.available_width());
ui.strong(&app.display_name);
ui.label(format!("App ID: {}", app.app_id));
if let Some(created) = &app.created_date_time {
ui.label(format!("Created: {}", created));
}
});
});
// Make the entire card clickable
if response.response.interact(egui::Sense::click()).clicked() && !is_selected {
*action = Some(AppListAction::SelectApp(app.clone()));
}
ui.add_space(5.0);
}
pub enum AppListAction {
Refresh,
SelectApp(Application),
CreateSecret,
SignOut,
}