// 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! {
}.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! {
| "Name" |
"IP" |
"Network" |
"Ports" |
"Applications" |
"Actions" |
{rows.into_iter().map(|host| {
let id = host.id;
view! {
|
{host.name}
|
{host.ip} |
{host.network_cidr}
|
{host.port_count} |
{host.application_count} |
|
}
}).collect_view()}
}.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()
}