// client/applications.rs — Applications list page // // Displays all applications with: // - Add button : opens a modal to create an application by name // - Filter bar : name substring filter (client-side) // - Table : application name + number of associated hosts // - Delete : confirmation modal before deletion use leptos::prelude::*; use leptos::form::ActionForm; use leptos::html::Input; use crate::api::applications::{ ApplicationWithCounts, CreateApplication, DeleteApplication, get_applications_with_counts, }; // ─── Add application modal ──────────────────────────────────────────────────── #[component] fn AddApplicationModal( create_action: ServerAction, show_modal: RwSignal, ) -> impl IntoView { use leptos::task::spawn_local; let name_ref = NodeRef::::new(); // Defer focus to the next microtask so the element is in the DOM. // Using get_untracked() avoids subscribing to NodeRef's reactive signal, // which would otherwise re-trigger during modal unmount and cause // "closure invoked after being dropped" in wasm-bindgen. spawn_local(async move { if let Some(el) = name_ref.get_untracked() { let _ = el.focus(); } }); // close() defers show_modal.set(false) to the next microtask. // Without this, setting the signal synchronously inside a click handler // unmounts the modal (and frees its closures) while the handler is still // on the call stack, causing wasm-bindgen to panic. let close = move || spawn_local(async move { show_modal.set(false) }); view! { }.into_any() } // ─── Delete confirmation modal ──────────────────────────────────────────────── #[component] fn DeleteAppModal( app: ApplicationWithCounts, delete_action: ServerAction, pending_delete: RwSignal>, ) -> impl IntoView { let id = app.id; let label = app.name.clone(); let host_count = app.host_count; view! { }.into_any() } // ─── Page ───────────────────────────────────────────────────────────────────── #[component] pub fn ApplicationsPage() -> impl IntoView { let create_action = ServerAction::::new(); let delete_action = ServerAction::::new(); let show_modal = RwSignal::new(false); // Some(app) = delete modal open for that app; None = closed. let pending_delete: RwSignal> = RwSignal::new(None); // Name filter (client-side — list is typically small) let name_filter = RwSignal::new(String::new()); // Close the add modal when the action transitions pending→done with Ok. // Lives in the parent so it is never recreated across modal open/close cycles, // which avoids the stale-value re-trigger bug. Effect::new(move |was_pending: Option| { let is_pending = create_action.pending().get(); if was_pending == Some(true) && !is_pending { if let Some(Ok(_)) = create_action.value().get() { show_modal.set(false); } } is_pending }); // Close the delete modal automatically after a successful deletion. Effect::new(move |_| { if let Some(Ok(_)) = delete_action.value().get() { pending_delete.set(None); } }); let applications = Resource::new( move || (create_action.version().get(), delete_action.version().get()), |_| get_applications_with_counts(), ); view! {
// ── Page header ─────────────────────────────────────────────────── // ── Add modal ───────────────────────────────────────────────────── {move || show_modal.get().then(|| view! { })} // ── Delete modal ────────────────────────────────────────────────── {move || pending_delete.get().map(|app| view! { })} // ── Filter bar ────────────────────────────────────────────────────
// ── Application list ──────────────────────────────────────────────
{move || delete_action.value().get() .and_then(|r| r.err()) .map(|e| view! {

"Delete failed: " {e.to_string()}

}) } "Loading applications…"

}> {move || applications.get().map(|result| match result { Err(e) => view! {

"Could not load applications: " {e.to_string()}

}.into_any(), Ok(list) => { let filter = name_filter.get().to_lowercase(); let filtered: Vec<_> = list.into_iter() .filter(|app| filter.is_empty() || app.name.to_lowercase().contains(&filter)) .collect(); if filtered.is_empty() { view! {

"No applications match the current filter."

}.into_any() } else { view! {
{filtered.into_iter().map(|app| { let app_clone = app.clone(); view! { } }).collect_view()}
"Name" "Hosts" "Actions"
{app.name} {app.host_count}
}.into_any() } } })}
} }