feat(db): add SQLx migrations and AppState with connection pool
- Add sqlx 0.8 (AnyPool, runtime-tokio, sqlite, postgres, migrate) - Create 6 migration files for both SQLite and PostgreSQL backends - Add server/db.rs: create_pool and run_migrations helpers - Add server/state.rs: AppState with LeptosOptions + AnyPool - Run migrations at server startup before accepting requests - Fix Port model: remove host_id (ports are now a global catalog) - Add HostPort join struct for the host_ports many-to-many table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
950
Cargo.lock
generated
950
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ ssr = [
|
|||||||
"dep:tracing-subscriber",
|
"dep:tracing-subscriber",
|
||||||
"dep:dotenvy",
|
"dep:dotenvy",
|
||||||
"dep:ipnetwork",
|
"dep:ipnetwork",
|
||||||
|
"dep:sqlx",
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos_meta/ssr",
|
"leptos_meta/ssr",
|
||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
@@ -68,6 +69,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = tr
|
|||||||
dotenvy = { version = "0.15", optional = true }
|
dotenvy = { version = "0.15", optional = true }
|
||||||
# Parsing et calcul de plages d'adresses IP (CIDR) — ex: 192.168.1.0/24
|
# Parsing et calcul de plages d'adresses IP (CIDR) — ex: 192.168.1.0/24
|
||||||
ipnetwork = { version = "0.20", optional = true }
|
ipnetwork = { version = "0.20", optional = true }
|
||||||
|
# Database access: connection pools, queries, migrations — SQLite + PostgreSQL
|
||||||
|
# "any" = runtime-dispatched driver (same code works with both backends)
|
||||||
|
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "sqlite", "postgres", "migrate", "any"], optional = true }
|
||||||
|
|
||||||
# --- Dépendances client uniquement (activées par la feature "hydrate") ---
|
# --- Dépendances client uniquement (activées par la feature "hydrate") ---
|
||||||
|
|
||||||
|
|||||||
6
migrations/postgres/0001_create_networks.sql
Normal file
6
migrations/postgres/0001_create_networks.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- networks: IP address ranges managed by the IPAM.
|
||||||
|
-- BIGSERIAL: auto-incrementing 64-bit integer (PostgreSQL's equivalent of AUTOINCREMENT).
|
||||||
|
CREATE TABLE IF NOT EXISTS networks (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
cidr TEXT NOT NULL UNIQUE
|
||||||
|
);
|
||||||
7
migrations/postgres/0002_create_hosts.sql
Normal file
7
migrations/postgres/0002_create_hosts.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- hosts: physical or virtual machines belonging to a network.
|
||||||
|
CREATE TABLE IF NOT EXISTS hosts (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
ip TEXT NOT NULL,
|
||||||
|
network_id BIGINT NOT NULL REFERENCES networks(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
5
migrations/postgres/0003_create_ports.sql
Normal file
5
migrations/postgres/0003_create_ports.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- ports: global catalog of TCP/UDP port numbers and their known protocol.
|
||||||
|
CREATE TABLE IF NOT EXISTS ports (
|
||||||
|
number INTEGER PRIMARY KEY,
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
5
migrations/postgres/0004_create_applications.sql
Normal file
5
migrations/postgres/0004_create_applications.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- applications: software stacks that use one or more ports.
|
||||||
|
CREATE TABLE IF NOT EXISTS applications (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
6
migrations/postgres/0005_create_host_ports.sql
Normal file
6
migrations/postgres/0005_create_host_ports.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- host_ports: which ports are open on which host (many-to-many).
|
||||||
|
CREATE TABLE IF NOT EXISTS host_ports (
|
||||||
|
host_id BIGINT NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
|
||||||
|
port_number INTEGER NOT NULL REFERENCES ports(number) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (host_id, port_number)
|
||||||
|
);
|
||||||
6
migrations/postgres/0006_create_application_ports.sql
Normal file
6
migrations/postgres/0006_create_application_ports.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- application_ports: which ports an application typically uses (many-to-many).
|
||||||
|
CREATE TABLE IF NOT EXISTS application_ports (
|
||||||
|
application_id BIGINT NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
||||||
|
port_number INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (application_id, port_number)
|
||||||
|
);
|
||||||
6
migrations/sqlite/0001_create_networks.sql
Normal file
6
migrations/sqlite/0001_create_networks.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- networks: IP address ranges managed by the IPAM.
|
||||||
|
-- Each network has a unique CIDR block (e.g. "192.168.1.0/24").
|
||||||
|
CREATE TABLE IF NOT EXISTS networks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
cidr TEXT NOT NULL UNIQUE
|
||||||
|
);
|
||||||
9
migrations/sqlite/0002_create_hosts.sql
Normal file
9
migrations/sqlite/0002_create_hosts.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- hosts: physical or virtual machines belonging to a network.
|
||||||
|
-- The ip field must fall within the CIDR of the parent network
|
||||||
|
-- (enforced in application code, not at the DB level).
|
||||||
|
CREATE TABLE IF NOT EXISTS hosts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
ip TEXT NOT NULL,
|
||||||
|
network_id INTEGER NOT NULL REFERENCES networks(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
7
migrations/sqlite/0003_create_ports.sql
Normal file
7
migrations/sqlite/0003_create_ports.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- ports: global catalog of TCP/UDP port numbers and their known protocol.
|
||||||
|
-- Ports are not tied to a specific host here — host_ports handles that link.
|
||||||
|
-- Port numbers range from 0 to 65535 (fits in INTEGER).
|
||||||
|
CREATE TABLE IF NOT EXISTS ports (
|
||||||
|
number INTEGER PRIMARY KEY,
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
6
migrations/sqlite/0004_create_applications.sql
Normal file
6
migrations/sqlite/0004_create_applications.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- applications: software stacks that use one or more ports.
|
||||||
|
-- Examples: "Nginx", "PostgreSQL", "Prometheus".
|
||||||
|
CREATE TABLE IF NOT EXISTS applications (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
7
migrations/sqlite/0005_create_host_ports.sql
Normal file
7
migrations/sqlite/0005_create_host_ports.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- host_ports: which ports are open on which host (many-to-many).
|
||||||
|
-- Composite primary key prevents duplicate (host, port) pairs.
|
||||||
|
CREATE TABLE IF NOT EXISTS host_ports (
|
||||||
|
host_id INTEGER NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
|
||||||
|
port_number INTEGER NOT NULL REFERENCES ports(number) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (host_id, port_number)
|
||||||
|
);
|
||||||
8
migrations/sqlite/0006_create_application_ports.sql
Normal file
8
migrations/sqlite/0006_create_application_ports.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-- application_ports: which ports an application typically uses (many-to-many).
|
||||||
|
-- port_number is not a strict FK to ports to allow registering an application
|
||||||
|
-- before its port entry exists in the catalog.
|
||||||
|
CREATE TABLE IF NOT EXISTS application_ports (
|
||||||
|
application_id INTEGER NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
||||||
|
port_number INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (application_id, port_number)
|
||||||
|
);
|
||||||
43
src/main.rs
43
src/main.rs
@@ -21,7 +21,12 @@ async fn main() {
|
|||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use rust_ipam::{
|
use rust_ipam::{
|
||||||
app::{App, Shell},
|
app::{App, Shell},
|
||||||
server::{config::AppConfig, routes::not_found_handler},
|
server::{
|
||||||
|
config::AppConfig,
|
||||||
|
db::{create_pool, run_migrations},
|
||||||
|
routes::not_found_handler,
|
||||||
|
state::AppState,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
@@ -44,6 +49,18 @@ async fn main() {
|
|||||||
|
|
||||||
tracing::info!("Database: {} ({})", app_config.backend, app_config.database_url);
|
tracing::info!("Database: {} ({})", app_config.backend, app_config.database_url);
|
||||||
|
|
||||||
|
// Connect to the database and apply any pending migrations.
|
||||||
|
// The server cannot serve data without a working database, so we abort on error.
|
||||||
|
let pool = create_pool(&app_config)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to database");
|
||||||
|
|
||||||
|
run_migrations(&pool, &app_config.backend)
|
||||||
|
.await
|
||||||
|
.expect("Database migration failed");
|
||||||
|
|
||||||
|
tracing::info!("Database ready.");
|
||||||
|
|
||||||
// `Some("Cargo.toml")` tells Leptos to read the [package.metadata.leptos]
|
// `Some("Cargo.toml")` tells Leptos to read the [package.metadata.leptos]
|
||||||
// section from Cargo.toml (file paths, output names, server address...).
|
// section from Cargo.toml (file paths, output names, server address...).
|
||||||
let conf = get_configuration(Some("Cargo.toml"))
|
let conf = get_configuration(Some("Cargo.toml"))
|
||||||
@@ -51,27 +68,37 @@ async fn main() {
|
|||||||
let leptos_options = conf.leptos_options;
|
let leptos_options = conf.leptos_options;
|
||||||
let addr = leptos_options.site_addr;
|
let addr = leptos_options.site_addr;
|
||||||
|
|
||||||
|
// Combine Leptos options and the database pool into a single shared state.
|
||||||
|
// `AppState` implements `FromRef<AppState> for LeptosOptions` so Leptos
|
||||||
|
// can still extract just what it needs from the full state.
|
||||||
|
let state = AppState { leptos_options: leptos_options.clone(), db: pool };
|
||||||
|
|
||||||
// Walk all `<Route>` components inside `App` to build the list of URLs
|
// Walk all `<Route>` components inside `App` to build the list of URLs
|
||||||
// that Leptos SSR must handle.
|
// that Leptos SSR must handle.
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
// Build the Axum router using the builder pattern (method chaining).
|
// Build the Axum router using the builder pattern (method chaining).
|
||||||
let app = Router::new()
|
//
|
||||||
|
// `Router::<AppState>::new()` explicitly tells Rust the state type is `AppState`.
|
||||||
|
// Without this annotation, type inference would default to `LeptosOptions`
|
||||||
|
// (inferred from `leptos_routes`) and then reject `.with_state(state: AppState)`.
|
||||||
|
let app = Router::<AppState>::new()
|
||||||
// Serve static files compiled by trunk (WASM, JS...).
|
// Serve static files compiled by trunk (WASM, JS...).
|
||||||
// Trunk places them in target/site/pkg/ as configured in [package.metadata.leptos].
|
// Trunk places them in target/site/pkg/ as configured in [package.metadata.leptos].
|
||||||
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
|
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
|
||||||
// Mount all Leptos routes into Axum.
|
// Mount all Leptos routes into Axum.
|
||||||
// For each URL, Axum renders Shell() to HTML and sends it to the browser.
|
// For each URL, Axum renders Shell() to HTML and sends it to the browser.
|
||||||
// Shell() contains App(), which provides the page content.
|
// `leptos_routes` receives `&state` (the full AppState).
|
||||||
.leptos_routes(&leptos_options, routes, {
|
// It extracts `LeptosOptions` via `FromRef<AppState>` implemented in state.rs.
|
||||||
// Clone options so the closure can capture them.
|
.leptos_routes(&state, routes, {
|
||||||
|
// Clone options before moving into the closure.
|
||||||
// `move` transfers ownership of `leptos_options` into the closure.
|
// `move` transfers ownership of `leptos_options` into the closure.
|
||||||
let leptos_options = leptos_options.clone();
|
let leptos_options = state.leptos_options.clone();
|
||||||
move || view! { <Shell options=leptos_options.clone()/> }
|
move || view! { <Shell options=leptos_options.clone()/> }
|
||||||
})
|
})
|
||||||
.fallback(not_found_handler)
|
.fallback(not_found_handler)
|
||||||
// Share Leptos options with all handlers via Axum's state system.
|
// Share AppState (Leptos options + DB pool) with all handlers.
|
||||||
.with_state(leptos_options);
|
.with_state(state);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&addr)
|
let listener = tokio::net::TcpListener::bind(&addr)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -57,24 +57,32 @@ pub struct Host {
|
|||||||
|
|
||||||
// ─── Port ─────────────────────────────────────────────────────────────────────
|
// ─── Port ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// A network port open on a host, with its likely protocol description.
|
/// A network port entry in the global port catalog.
|
||||||
///
|
///
|
||||||
|
/// Ports are defined once here; host_ports and application_ports link them
|
||||||
|
/// to hosts and applications through separate join tables.
|
||||||
/// Well-known ports (0–1023) have standardized protocol assignments.
|
/// Well-known ports (0–1023) have standardized protocol assignments.
|
||||||
/// A port can be associated with multiple applications (non-strict relation).
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Port {
|
pub struct Port {
|
||||||
/// TCP/UDP port number.
|
/// TCP/UDP port number (0–65535).
|
||||||
/// `u16` is an unsigned 16-bit integer → range 0 to 65535,
|
/// `u16` is an unsigned 16-bit integer — the exact range for port numbers.
|
||||||
/// which exactly matches the valid range for network ports.
|
|
||||||
pub number: u16,
|
pub number: u16,
|
||||||
|
|
||||||
/// Description of the likely protocol on this port.
|
/// Description of the protocol typically running on this port.
|
||||||
/// `Option<String>`: may be absent (None) when the protocol is unknown.
|
/// `Option<String>`: absent (None) when the protocol is unknown.
|
||||||
/// Examples: Some("SSH"), Some("HTTPS"), None
|
/// Examples: Some("SSH"), Some("HTTPS"), None
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The host on which this port is open.
|
// ─── HostPort ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Join record representing a port open on a specific host.
|
||||||
|
///
|
||||||
|
/// Maps to the `host_ports` table (many-to-many between hosts and ports).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct HostPort {
|
||||||
pub host_id: i64,
|
pub host_id: i64,
|
||||||
|
pub port_number: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Port {
|
impl Port {
|
||||||
|
|||||||
68
src/server/db.rs
Normal file
68
src/server/db.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// server/db.rs — Database connection pool and migrations
|
||||||
|
//
|
||||||
|
// This module provides two functions called once at server startup:
|
||||||
|
// - `create_pool` : opens a connection pool to the database
|
||||||
|
// - `run_migrations` : applies all pending SQL migrations
|
||||||
|
//
|
||||||
|
// `AnyPool` lets the same Rust code target both SQLite (dev) and
|
||||||
|
// PostgreSQL (production) — only DATABASE_URL changes.
|
||||||
|
|
||||||
|
use sqlx::AnyPool;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::server::config::{AppConfig, DatabaseBackend};
|
||||||
|
|
||||||
|
// ─── Errors ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DbError {
|
||||||
|
#[error("Database connection failed: {0}")]
|
||||||
|
Connection(#[from] sqlx::Error),
|
||||||
|
|
||||||
|
#[error("Migration failed: {0}")]
|
||||||
|
Migration(#[from] sqlx::migrate::MigrateError),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Pool creation ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Opens a connection pool to the database specified in `config.database_url`.
|
||||||
|
///
|
||||||
|
/// A pool maintains multiple open connections so concurrent requests
|
||||||
|
/// do not block each other waiting for a single connection.
|
||||||
|
///
|
||||||
|
/// `install_default_drivers()` must be called before `AnyPool::connect`
|
||||||
|
/// to register both the SQLite and PostgreSQL drivers in the `Any` registry.
|
||||||
|
pub async fn create_pool(config: &AppConfig) -> Result<AnyPool, DbError> {
|
||||||
|
// Register SQLite and PostgreSQL drivers so `AnyPool` can dispatch
|
||||||
|
// to the correct one based on the URL scheme (sqlite:// vs postgres://).
|
||||||
|
sqlx::any::install_default_drivers();
|
||||||
|
|
||||||
|
let pool = AnyPool::connect(&config.database_url).await?;
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Migrations ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Applies all pending migrations from the directory matching the active backend.
|
||||||
|
///
|
||||||
|
/// Two separate directories handle SQL syntax differences:
|
||||||
|
/// - `migrations/sqlite/` : uses `INTEGER PRIMARY KEY AUTOINCREMENT`
|
||||||
|
/// - `migrations/postgres/` : uses `BIGSERIAL PRIMARY KEY`
|
||||||
|
///
|
||||||
|
/// SQLx tracks applied migrations in a `_sqlx_migrations` table, so running
|
||||||
|
/// this function on an already-migrated database is always safe (idempotent).
|
||||||
|
///
|
||||||
|
/// `sqlx::migrate!("path")` is a compile-time macro: it embeds all `.sql`
|
||||||
|
/// files from the given path directly into the binary. The path is relative
|
||||||
|
/// to the project root (where Cargo.toml lives).
|
||||||
|
pub async fn run_migrations(pool: &AnyPool, backend: &DatabaseBackend) -> Result<(), DbError> {
|
||||||
|
match backend {
|
||||||
|
DatabaseBackend::Sqlite => {
|
||||||
|
sqlx::migrate!("migrations/sqlite").run(pool).await?;
|
||||||
|
}
|
||||||
|
DatabaseBackend::Postgres => {
|
||||||
|
sqlx::migrate!("migrations/postgres").run(pool).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -11,5 +11,11 @@ pub mod routes;
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod db;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod validation;
|
pub mod validation;
|
||||||
|
|||||||
42
src/server/state.rs
Normal file
42
src/server/state.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// server/state.rs — Shared Axum application state
|
||||||
|
//
|
||||||
|
// Axum uses a typed state system: any handler can extract a specific type
|
||||||
|
// from the shared state using the `State<T>` extractor.
|
||||||
|
//
|
||||||
|
// We store two pieces of state:
|
||||||
|
// - `leptos_options` : required by Leptos SSR routes
|
||||||
|
// - `db` : database pool shared across all requests
|
||||||
|
|
||||||
|
use axum::extract::FromRef;
|
||||||
|
use leptos::config::LeptosOptions;
|
||||||
|
use sqlx::AnyPool;
|
||||||
|
|
||||||
|
// ─── AppState ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Shared state available to every Axum handler and Leptos server function.
|
||||||
|
///
|
||||||
|
/// `#[derive(Clone)]` is required by Axum: each request receives its own clone.
|
||||||
|
/// Both fields are cheap to clone — they hold reference-counted pointers
|
||||||
|
/// under the hood, so cloning just increments an atomic counter.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
/// Leptos configuration (output paths, site address, reload port…).
|
||||||
|
pub leptos_options: LeptosOptions,
|
||||||
|
|
||||||
|
/// Shared connection pool to the database.
|
||||||
|
/// Using a pool (not a single connection) allows concurrent requests
|
||||||
|
/// to run their queries in parallel without contention.
|
||||||
|
pub db: AnyPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── FromRef implementations ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Tells Axum how to extract just `LeptosOptions` from the full `AppState`.
|
||||||
|
///
|
||||||
|
/// `.leptos_routes()` in main.rs requires this impl because it stores
|
||||||
|
/// only `LeptosOptions` internally, not the full application state.
|
||||||
|
impl FromRef<AppState> for LeptosOptions {
|
||||||
|
fn from_ref(state: &AppState) -> Self {
|
||||||
|
state.leptos_options.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user