fix(hosts): use LocalResource for network/app dropdowns to fix hydration blank

Resource::new() with SSR returns None during hydration outside <Suspense>,
causing dropdowns to stay empty on direct page load. LocalResource fetches
client-side only, bypassing the hydration mismatch entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 02:08:53 +02:00
parent eef0ae0b54
commit d2284727a2

View File

@@ -32,7 +32,7 @@ const PER_PAGE_OPTIONS: &[(i64, &str)] = &[
#[component] #[component]
fn AddHostModal( fn AddHostModal(
create_action: ServerAction<CreateHost>, create_action: ServerAction<CreateHost>,
networks_res: Resource<Result<Vec<crate::models::Network>, ServerFnError>>, networks_res: LocalResource<Result<Vec<crate::models::Network>, ServerFnError>>,
show_modal: RwSignal<bool>, show_modal: RwSignal<bool>,
) -> impl IntoView { ) -> impl IntoView {
// Close the modal automatically after a successful creation. // Close the modal automatically after a successful creation.
@@ -70,7 +70,7 @@ fn AddHostModal(
<select name="network_id" required> <select name="network_id" required>
<option value="">"— choose —"</option> <option value="">"— choose —"</option>
{move || networks_res.get() {move || networks_res.get()
.and_then(|r| r.ok()) .and_then(|r| (*r).clone().ok())
.map(|nets| nets.into_iter().map(|n| { .map(|nets| nets.into_iter().map(|n| {
let label = format!("{} - {}", n.name, n.cidr); let label = format!("{} - {}", n.name, n.cidr);
view! { <option value=n.id.to_string()>{label}</option> } view! { <option value=n.id.to_string()>{label}</option> }
@@ -112,8 +112,8 @@ fn AddHostModal(
#[component] #[component]
fn FilterBar( fn FilterBar(
networks_res: Resource<Result<Vec<crate::models::Network>, ServerFnError>>, networks_res: LocalResource<Result<Vec<crate::models::Network>, ServerFnError>>,
applications_res: Resource<Result<Vec<crate::models::Application>, ServerFnError>>, applications_res: LocalResource<Result<Vec<crate::models::Application>, ServerFnError>>,
name_filter: RwSignal<String>, name_filter: RwSignal<String>,
network_id_filter: RwSignal<i64>, network_id_filter: RwSignal<i64>,
port_filter: RwSignal<String>, port_filter: RwSignal<String>,
@@ -143,7 +143,7 @@ fn FilterBar(
}> }>
<option value="0">"All networks"</option> <option value="0">"All networks"</option>
{move || networks_res.get() {move || networks_res.get()
.and_then(|r| r.ok()) .and_then(|r| (*r).clone().ok())
.map(|nets| nets.into_iter().map(|n| { .map(|nets| nets.into_iter().map(|n| {
let label = format!("{} - {}", n.name, n.cidr); let label = format!("{} - {}", n.name, n.cidr);
view! { <option value=n.id.to_string()>{label}</option> } view! { <option value=n.id.to_string()>{label}</option> }
@@ -172,7 +172,7 @@ fn FilterBar(
}> }>
<option value="0">"All applications"</option> <option value="0">"All applications"</option>
{move || applications_res.get() {move || applications_res.get()
.and_then(|r| r.ok()) .and_then(|r| (*r).clone().ok())
.map(|apps| apps.into_iter().map(|a| { .map(|apps| apps.into_iter().map(|a| {
view! { <option value=a.id.to_string()>{a.name}</option> } view! { <option value=a.id.to_string()>{a.name}</option> }
}).collect_view()) }).collect_view())
@@ -342,8 +342,8 @@ pub fn HostsPage() -> impl IntoView {
|(name, net, port, app, p, pp, _, _)| get_hosts_page(name, net, port, app, p, pp), |(name, net, port, app, p, pp, _, _)| get_hosts_page(name, net, port, app, p, pp),
); );
let networks_res = Resource::new(|| (), |_| get_networks()); let networks_res = LocalResource::new(|| get_networks());
let applications_res = Resource::new(|| (), |_| get_applications()); let applications_res = LocalResource::new(|| get_applications());
let total_pages = Signal::derive(move || { let total_pages = Signal::derive(move || {
hosts.get().and_then(|r| r.ok()).map(|p| p.total_pages).unwrap_or(1) hosts.get().and_then(|r| r.ok()).map(|p| p.total_pages).unwrap_or(1)