From 54d6e875fb541ce1e0da84128d5fd29546e423b9 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Wed, 3 Jan 2024 21:11:11 +0100 Subject: Move UI into src/ui --- src/ui.rs | 230 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 src/ui.rs (limited to 'src/ui.rs') diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..aebcd4a --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,230 @@ +use std::str::FromStr; +use serde::Deserialize; +use askama::Template; +use axum::{ + extract::State, + routing::get, + Router, + response::Html, + Form, + http::StatusCode, +}; +use tower_http::services::ServeDir; + +use crate::config; +use crate::SharedState; + +pub async fn serve(port: u16, shared_state: SharedState) { + let app = Router::new() + .route("/", get(dashboard)) + .route("/incoming", get(incoming)) + .route("/send", get(send)) + .route("/settings", get(show_settings).post(post_settings)) + .nest_service("/static", ServeDir::new("static")) + /* requires tracing and tower, e.g. + * tower = { version = "0.4", features = ["util", "timeout"] } + * tower-http = { version = "0.5.0", features = ["add-extension", "trace"] } + * tracing = "0.1" + * tracing-subscriber = { version = "0.3", features = ["env-filter"] } + .layer( + ServiceBuilder::new() + .layer(HandleErrorLayer::new(|error: BoxError| async move { + ,if error.is::() { + Ok(StatusCode::REQUEST_TIMEOUT) + } else { + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled internal error: {error}"), + )) + } + })) + .timeout(Duration::from_secs(10)) + .layer(TraceLayer::new_for_http()) + .into_inner(), + )*/ + .with_state(shared_state); + + let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await.unwrap(); + axum::serve(listener, app).await.unwrap() +} + +#[derive(PartialEq)] +enum ActivePage { + Dashboard, + Incoming, + Send, + Settings, +} + +#[derive(Template)] +#[template(path = "dashboard.html")] +struct DashboardTemplate<'a> { + title: &'a str, + page: ActivePage, + conf: config::Config, +} + +async fn dashboard(State(state): State) -> DashboardTemplate<'static> { + DashboardTemplate { + title: "Dashboard", + conf: state.lock().unwrap().conf.clone(), + page: ActivePage::Dashboard, + } +} + +#[derive(Template)] +#[template(path = "incoming.html")] +struct IncomingTemplate<'a> { + title: &'a str, + page: ActivePage, + conf: config::Config, +} + +async fn incoming(State(state): State) -> IncomingTemplate<'static> { + IncomingTemplate { + title: "Incoming", + conf: state.lock().unwrap().conf.clone(), + page: ActivePage::Incoming, + } +} + +#[derive(Template)] +#[template(path = "send.html")] +struct SendTemplate<'a> { + title: &'a str, + page: ActivePage, + conf: config::Config, +} + +async fn send(State(state): State) -> SendTemplate<'static> { + SendTemplate { + title: "Send", + conf: state.lock().unwrap().conf.clone(), + page: ActivePage::Send, + } +} + +#[derive(Template)] +#[template(path = "settings.html")] +struct SettingsTemplate<'a> { + title: &'a str, + page: ActivePage, + conf: config::Config, +} + +async fn show_settings(State(state): State) -> SettingsTemplate<'static> { + SettingsTemplate { + title: "Settings", + page: ActivePage::Settings, + conf: state.lock().unwrap().conf.clone(), + } +} + +#[derive(Deserialize, Debug)] +struct FormConfig { + callsign: String, + ssid: String, + icon: String, + + // felinet + // felinet_enabled is either "on" or absent. + // According to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input/checkbox + // "If the value attribute was omitted, the default value for the checkbox is `on` [...]" + felinet_enabled: Option, + address: String, + + // beacon + period_seconds: config::DurationSeconds, + max_hops: u8, + latitude: String, + longitude: String, + altitude: String, + comment: String, + antenna_height: String, + antenna_gain: String, + tx_power: String, + + // tunnel + tunnel_enabled: Option, + local_ip: String, + netmask: String, +} + +fn empty_string_to_none(value: &str) -> Result, T::Err> { + if value == "" { + Ok(None) + } + else { + Ok(Some(value.parse()?)) + } +} + +impl TryFrom for config::Config { + type Error = anyhow::Error; + + fn try_from(value: FormConfig) -> Result { + Ok(config::Config { + callsign: value.callsign, + ssid: value.ssid.parse()?, + icon: value.icon.parse()?, + felinet: config::FelinetConfig { + enabled: value.felinet_enabled.is_some(), + address: value.address, + }, + beacon: config::BeaconConfig { + period_seconds: value.period_seconds, + max_hops: value.max_hops, + latitude: empty_string_to_none(&value.latitude)?, + longitude: empty_string_to_none(&value.longitude)?, + altitude: empty_string_to_none(&value.altitude)?, + comment: empty_string_to_none(&value.comment)?, + antenna_height: empty_string_to_none(&value.antenna_height)?, + antenna_gain: empty_string_to_none(&value.antenna_gain)?, + tx_power: empty_string_to_none(&value.tx_power)?, + }, + tunnel: config::TunnelConfig { + enabled: value.tunnel_enabled.is_some(), + local_ip: value.local_ip, + netmask: value.netmask, + }, + }) + } +} + +async fn post_settings(State(state): State, Form(input): Form) -> (StatusCode, Html) { + match config::Config::try_from(input) { + Ok(c) => { + match c.store() { + Ok(()) => { + state.lock().unwrap().conf.clone_from(&c); + + (StatusCode::OK, Html( + r#" + +

Configuration updated

+

To dashboard

+ "#.to_owned())) + } + Err(e) => { + (StatusCode::INTERNAL_SERVER_ERROR, Html( + format!(r#" + +

Internal Server Error: Could not write config

+

{}

+ + "#, e))) + }, + } + + }, + Err(e) => { + (StatusCode::BAD_REQUEST, Html( + format!(r#" + +

Error interpreting POST data

+

{}

+ + "#, e))) + }, + } +} -- cgit v1.2.3