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(), + })} +
} }