// client/hosts.rs — Hosts list page // // Displays all hosts across every network with: // - Add button : opens a modal form to create a host inside a chosen network // - Filter bar : name (substring), network, open ports (CSV), application // - Table : name, IP, network, port count, application count, delete // - Pagination : configurable page size (15 / 25 / 50 / 100 / All) // // Sub-components call `.into_any()` on their views to erase the concrete // Leptos type, preventing the parent from accumulating a deeply-nested // generic type that overflows the compiler's query depth limit. use leptos::prelude::*; use leptos::form::ActionForm; use crate::api::{ applications::get_applications, hosts::{CreateHost, DeleteHost, get_hosts_page, HostsPage as HostsPageData}, networks::get_networks, }; const PER_PAGE_OPTIONS: &[(i64, &str)] = &[ (15, "15"), (25, "25"), (50, "50"), (100, "100"), (0, "All"), ]; // ─── Add host modal ─────────────────────────────────────────────────────────── #[component] fn AddHostModal( create_action: ServerAction, networks_res: Resource, ServerFnError>>, show_modal: RwSignal, ) -> impl IntoView { // Close the modal automatically after a successful creation. Effect::new(move |_| { if let Some(Ok(_)) = create_action.value().get() { show_modal.set(false); } }); view! { }.into_any() } // ─── Filter bar ─────────────────────────────────────────────────────────────── #[component] fn FilterBar( networks_res: Resource, ServerFnError>>, applications_res: Resource, ServerFnError>>, name_filter: RwSignal, network_id_filter: RwSignal, port_filter: RwSignal, app_id_filter: RwSignal, page: RwSignal, ) -> impl IntoView { view! {
}.into_any() } // ─── Pagination bar ─────────────────────────────────────────────────────────── #[component] fn PaginationBar( total: Signal, page: RwSignal, per_page: RwSignal, total_pages: Signal, ) -> impl IntoView { view! {
{move || { let t = total.get(); if t == 0 { "No hosts found".to_string() } else { format!("{} host{}", t, if t == 1 { "" } else { "s" }) } }}
{move || (per_page.get() > 0).then(|| view! {
{move || format!("Page {} of {}", page.get(), total_pages.get().max(1))}
})}
}.into_any() } // ─── Host table ─────────────────────────────────────────────────────────────── #[component] fn HostTable( hosts: Resource>, delete_action: ServerAction, ) -> impl IntoView { view! { "Loading hosts…"

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

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

}.into_any(), Ok(HostsPageData { rows, .. }) if rows.is_empty() => view! {

"No hosts match the current filters."

}.into_any(), Ok(HostsPageData { rows, .. }) => view! {
{rows.into_iter().map(|host| { let id = host.id; view! { } }).collect_view()}
"Name" "IP" "Network" "Ports" "Applications" "Actions"
{host.name} {host.ip} {host.network_cidr} {host.port_count} {host.application_count}
}.into_any(), })}
}.into_any() } // ─── Main page component ────────────────────────────────────────────────────── #[component] pub fn HostsPage() -> impl IntoView { let create_action = ServerAction::::new(); let delete_action = ServerAction::::new(); let show_modal = RwSignal::new(false); // Filter signals ("" / 0 = no filter) let name_filter = RwSignal::new(String::new()); let network_id_filter = RwSignal::new(0i64); let port_filter = RwSignal::new(String::new()); // CSV of port numbers let app_id_filter = RwSignal::new(0i64); let page = RwSignal::new(1i64); let per_page = RwSignal::new(15i64); let hosts = Resource::new( move || ( name_filter.get(), network_id_filter.get(), port_filter.get(), app_id_filter.get(), page.get(), per_page.get(), create_action.version().get(), delete_action.version().get(), ), |(name, net, port, app, p, pp, _, _)| get_hosts_page(name, net, port, app, p, pp), ); let networks_res = Resource::new(|| (), |_| get_networks()); let applications_res = Resource::new(|| (), |_| get_applications()); let total_pages = Signal::derive(move || { hosts.get().and_then(|r| r.ok()).map(|p| p.total_pages).unwrap_or(1) }); let total = Signal::derive(move || { hosts.get().and_then(|r| r.ok()).map(|p| p.total).unwrap_or(0) }); view! {
{move || show_modal.get().then(|| view! { })}
{move || delete_action.value().get() .and_then(|r| r.err()) .map(|e| view! {

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

}) }
}.into_any() }