From eec01c9b72feff9533477014881b982124ca7b6d Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Thu, 19 Sep 2024 22:09:20 +0200 Subject: Create project --- src/config.rs | 36 +++++++++++++ src/main.rs | 31 ++++++++++++ src/ui.rs | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 src/config.rs create mode 100644 src/main.rs create mode 100644 src/ui.rs (limited to 'src') diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..3e0571d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,36 @@ +use std::fs; +use anyhow::Context; +use serde::{Deserialize, Serialize}; + + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Config { + pub name: String, +} + +impl Default for Config { + fn default() -> Self { + Config { + name: "CHANGEME".to_owned(), + } + } +} + +const CONFIGFILE : &str = "odr-dabmux-gui-config.toml"; + +impl Config { + pub fn load() -> anyhow::Result { + if std::path::Path::new(CONFIGFILE).exists() { + let file_contents = fs::read_to_string(CONFIGFILE)?; + toml::from_str(&file_contents).context("parsing config file") + } + else { + Ok(Default::default()) + } + } + + pub fn store(&self) -> anyhow::Result<()> { + fs::write(CONFIGFILE, toml::to_string_pretty(&self)?) + .context("writing config file") + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e64570a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,31 @@ +use std::sync::{Arc, Mutex}; +use anyhow::{anyhow, Context}; +use log::{debug, info, warn, error}; + +mod ui; +mod config; + +struct AppState { + conf : config::Config, +} + +type SharedState = Arc>; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + simple_logger::SimpleLogger::new() + .with_level(log::LevelFilter::Debug) + .env() + .init().unwrap(); + + let conf = config::Config::load().expect("Could not load config"); + + let shared_state = Arc::new(Mutex::new(AppState { + conf : conf.clone(), + })); + + let port = 3000; + info!("Setting up listener on port {port}"); + ui::serve(port, shared_state).await; + Ok(()) +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..529dfa6 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,158 @@ +use std::net::SocketAddr; +use anyhow::{anyhow, Context}; +use askama::Template; +use axum::{ + Form, + Json, + Router, + extract::State, + extract::{ws::{Message, WebSocket, WebSocketUpgrade}, ConnectInfo}, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, +}; +use serde::Deserialize; + +use log::{debug, info, warn, error}; +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("/settings", get(show_settings).post(post_settings)) + .nest_service("/static", ServeDir::new("static")) + /* For an example for timeouts and tracing, have a look at the git history */ + .with_state(shared_state); + + let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await.unwrap(); + axum::serve(listener, + app.into_make_service_with_connect_info::()) + .await.unwrap() +} + +#[derive(PartialEq)] +enum ActivePage { + Dashboard, + Settings, + None, +} + +impl ActivePage { + // Used by templates/head.html to include the correct js files in + fn styles(&self) -> Vec<&'static str> { + match self { + ActivePage::Dashboard => vec![], + ActivePage::Settings => vec![], + ActivePage::None => vec![], + } + } +} + +#[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> { + let conf = { + let st = state.lock().unwrap(); + st.conf.clone() + }; + + DashboardTemplate { + title: "Dashboard", + conf, + page: ActivePage::Dashboard, + } +} + +#[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(Template)] +#[template(path = "settings_applied.html")] +struct SettingsAppliedTemplate<'a> { + title: &'a str, + page: ActivePage, + conf: config::Config, + ok: bool, + error_message: &'a str, + error_reason: String, +} + +#[derive(Deserialize, Debug)] +struct FormConfig { + name: String, +} + +impl TryFrom for config::Config { + type Error = anyhow::Error; + + fn try_from(value: FormConfig) -> Result { + Ok(config::Config { + name: value.name, + }) + } +} + +async fn post_settings( + State(state): State, + Form(input): Form) -> (StatusCode, SettingsAppliedTemplate<'static>) { + match config::Config::try_from(input) { + Ok(c) => { + match c.store() { + Ok(()) => { + state.lock().unwrap().conf.clone_from(&c); + + (StatusCode::OK, SettingsAppliedTemplate { + title: "Settings", + conf: c, + page: ActivePage::None, + ok: true, + error_message: "", + error_reason: "".to_owned(), + }) + } + Err(e) => { + (StatusCode::INTERNAL_SERVER_ERROR, SettingsAppliedTemplate { + title: "Settings", + conf : c, + page: ActivePage::None, + ok: false, + error_message: "Failed to store config", + error_reason: e.to_string(), + }) + }, + } + }, + Err(e) => { + (StatusCode::BAD_REQUEST, SettingsAppliedTemplate { + title: "Settings", + conf: state.lock().unwrap().conf.clone(), + page: ActivePage::None, + ok: false, + error_message: "Error interpreting POST data", + error_reason: e.to_string(), + }) + }, + } +} -- cgit v1.2.3