From 3ee39b96bb61b96c8994f8f4ab507f5bac73ae92 Mon Sep 17 00:00:00 2001 From: mathieu Date: Fri, 15 May 2026 22:49:25 +0200 Subject: [PATCH] feat(home): replace demo with entity count dashboard Home page now shows one clickable summary card per entity type (Networks, Hosts, Applications). Each card displays the total count fetched from the database via a single get_summary() server function, then navigates to the corresponding page on click. Counts are pre-rendered server-side via so the page is useful even before the WASM bundle loads. Co-Authored-By: Claude Sonnet 4.6 --- src/api/mod.rs | 1 + src/api/summary.rs | 45 +++++++++++++++++ src/client/home.rs | 120 +++++++++++++++++++++------------------------ 3 files changed, 103 insertions(+), 63 deletions(-) create mode 100644 src/api/summary.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index 0e5a030..52a943b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -17,3 +17,4 @@ pub mod applications; pub mod hosts; pub mod networks; +pub mod summary; diff --git a/src/api/summary.rs b/src/api/summary.rs new file mode 100644 index 0000000..692cefd --- /dev/null +++ b/src/api/summary.rs @@ -0,0 +1,45 @@ +// api/summary.rs — Dashboard summary server function +// +// Returns the count of each main entity in a single server round-trip. +// The home page uses this to display a quick-glance dashboard without +// loading the full list of each entity. + +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +// Counts for every top-level entity shown on the dashboard. +// Add a field here when a new entity type is introduced. +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Summary { + pub network_count: i64, + pub host_count: i64, + pub application_count: i64, +} + +#[server] +pub async fn get_summary() -> Result { + use sqlx::AnyPool; + use leptos::prelude::use_context; + + let pool = use_context::() + .ok_or_else(|| ServerFnError::new("Database pool not found in context"))?; + + // Three lightweight COUNT queries — no full table scans on the payload side. + // sqlx returns COUNT(*) as i64 for both SQLite and PostgreSQL. + let network_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM networks") + .fetch_one(&pool) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + + let host_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM hosts") + .fetch_one(&pool) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + + let application_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM applications") + .fetch_one(&pool) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + + Ok(Summary { network_count, host_count, application_count }) +} diff --git a/src/client/home.rs b/src/client/home.rs index a2dfdb9..6748d19 100644 --- a/src/client/home.rs +++ b/src/client/home.rs @@ -1,78 +1,72 @@ -// client/home.rs — Home page +// client/home.rs — Dashboard home page // -// Demonstrates the core Leptos concepts: -// - Components (`#[component]`) -// - Signals (reactive values) -// - Memos (derived values) -// - Event handling (`on:click`) +// Shows a quick-glance summary card for each main entity (Networks, Hosts, +// Applications). Each card displays the total count and navigates to the +// corresponding page on click. +// +// The counts are fetched via a single `get_summary()` server function call +// so the page only makes one round-trip to the server. use leptos::prelude::*; +use crate::api::summary::get_summary; -// Home page component. +// A single summary card — count + label + link. // -// In Leptos, a component is a plain Rust function annotated with `#[component]`. -// It is called once to build the reactive graph — -// unlike React, it does not re-execute on every update. +// `href` : navigation target when the card is clicked +// `label` : entity name displayed on the card +// `count` : the number to show, or None while loading +#[component] +fn SummaryCard(href: &'static str, label: &'static str, count: Signal>) -> impl IntoView { + view! { + + + {move || match count.get() { + Some(n) => n.to_string(), + None => "—".to_string(), + }} + + {label} + + } +} + #[component] pub fn HomePage() -> impl IntoView { - // `RwSignal` (Read-Write Signal) is a mutable reactive value. - // - // When `.set()` or `.update()` is called, Leptos automatically identifies - // all DOM nodes that depend on this signal and updates only those — - // no Virtual DOM, no full diff: surgical updates only. - // - // `i32` = signed 32-bit integer (Rust's default integer type) - let counter = RwSignal::new(0i32); - - // `Memo` is a value derived from one or more signals. - // It recomputes automatically when `counter` changes, but only - // notifies its dependents when the result actually differs. - // - // `move |_|`: a closure that captures `counter` by move (takes ownership). - // - `move` : the closure owns `counter` - // - `|_|` : ignores the argument (the previous memo value) - let doubled = Memo::new(move |_| counter.get() * 2); + // Fetch all counts in a single server call. + // Source `|| ()` is constant — the fetcher runs exactly once on mount. + let summary = Resource::new(|| (), |_| get_summary()); view! {
-

"Rust IPAM"

-

"IP Address Manager"

+
+

"Rust IPAM"

+

"IP Address Manager"

+
- // --- Reactivity demo --- - // In the real IPAM app, this section will show the network list, - // available addresses, usage statistics, etc. -
-

"Leptos Reactivity Demo"

- - // `{counter}` inserts the signal value directly into the DOM. - // Leptos updates ONLY this text node when counter changes — - // the entire component does not re-render. -

"Counter: " {counter}

- - // `{doubled}`: same, but for the memo (derived value) -

"Doubled: " {doubled}

- -
- // `on:click` attaches a JavaScript event listener. - // - // `.update(|n| *n += 1)`: - // - takes a closure receiving a `&mut i32` - // - `*n` dereferences the pointer to modify the value - // - `+= 1` increments in place - - - - - // `.set(0)` replaces the value directly (simpler than update here) - + // `` lets the SSR server wait for the resource before sending + // HTML, so the counts are already filled in on first page load. + // The fallback is only shown in the browser while the WASM loads + // (for users with slow connections). + +
"—""Networks"
+
"—""Hosts"
+
"—""Applications"
-
+ }> + {move || summary.get().map(|result| match result { + Err(e) => view! { +

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

+ }.into_any(), + Ok(s) => view! { +
+ + + +
+ }.into_any(), + })} +
} }