From 1e5019161d379424fe223b236899dcc777c5c9ae Mon Sep 17 00:00:00 2001 From: Paul-Nicolas Madelaine Date: Sat, 14 Oct 2023 00:06:04 +0200 Subject: [PATCH] webapp: urls, settings --- Cargo.lock | 1 - nix/packages/webapp.nix | 1 - nix/packages/webroot.nix | 13 +- typhon-webapp/Cargo.toml | 1 - typhon-webapp/index.html | 3 +- typhon-webapp/src/appurl.rs | 49 ------- typhon-webapp/src/drv_log.rs | 9 +- typhon-webapp/src/evaluation.rs | 52 +++---- typhon-webapp/src/home.rs | 42 +++--- typhon-webapp/src/job.rs | 30 ++-- typhon-webapp/src/jobset.rs | 47 +++--- typhon-webapp/src/lib.rs | 247 ++++++++++++++++---------------- typhon-webapp/src/login.rs | 20 ++- typhon-webapp/src/main.rs | 55 +------ typhon-webapp/src/project.rs | 38 ++--- 15 files changed, 261 insertions(+), 347 deletions(-) delete mode 100644 typhon-webapp/src/appurl.rs diff --git a/Cargo.lock b/Cargo.lock index f9dcdded..e4220965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3496,7 +3496,6 @@ dependencies = [ "gloo-net", "gloo-storage", "gloo-utils 0.2.0", - "once_cell", "seed", "serde", "serde-wasm-bindgen 0.4.5", diff --git a/nix/packages/webapp.nix b/nix/packages/webapp.nix index cd704725..39843059 100644 --- a/nix/packages/webapp.nix +++ b/nix/packages/webapp.nix @@ -37,6 +37,5 @@ in trunkIndexPath = "typhon-webapp/index.html"; preBuild = '' ln -s ${nodeDependencies}/lib/node_modules typhon-webapp/node_modules - echo "build.public_url = \"WEBROOT\"" >> Trunk.toml ''; } diff --git a/nix/packages/webroot.nix b/nix/packages/webroot.nix index a75061fd..86419af8 100644 --- a/nix/packages/webroot.nix +++ b/nix/packages/webroot.nix @@ -22,17 +22,18 @@ in baseurl ? "127.0.0.1:8000/api", https ? false, }: let - settings = pkgs.writeTextFile { - name = "settings.json"; - text = builtins.toJSON {inherit baseurl https;}; - }; + settings = builtins.toJSON {inherit baseurl https;}; in pkgs.stdenv.mkDerivation { name = "typhon-webroot"; src = webapp; buildPhase = '' - substituteInPlace ./index.html --replace "WEBROOT" "${webroot}/" - cp ${settings} settings.json + substituteInPlace ./index.html --replace \ + '' \ + '' + substituteInPlace ./index.html --replace \ + '' \ + '' cp ${tarball} source.tar.gz ''; installPhase = '' diff --git a/typhon-webapp/Cargo.toml b/typhon-webapp/Cargo.toml index 70981a22..e114859e 100644 --- a/typhon-webapp/Cargo.toml +++ b/typhon-webapp/Cargo.toml @@ -19,7 +19,6 @@ gloo-console.workspace = true gloo-net.workspace = true gloo-storage.workspace = true gloo-utils.workspace = true -once_cell.workspace = true seed.workspace = true serde-wasm-bindgen.workspace = true serde.workspace = true diff --git a/typhon-webapp/index.html b/typhon-webapp/index.html index 4dbb0dce..91711527 100644 --- a/typhon-webapp/index.html +++ b/typhon-webapp/index.html @@ -1,10 +1,11 @@ - + Typhon +
diff --git a/typhon-webapp/src/appurl.rs b/typhon-webapp/src/appurl.rs deleted file mode 100644 index 580e625f..00000000 --- a/typhon-webapp/src/appurl.rs +++ /dev/null @@ -1,49 +0,0 @@ -#[derive(Clone, Debug, Default)] -pub struct AppUrl { - pub chunks: Vec, -} -impl From for seed::Url { - fn from(url: AppUrl) -> seed::Url { - url.chunks.iter().fold( - seed::Url::new().set_path(crate::webroot_chunks()), - |url, chunk| url.add_path_part(chunk), - ) - } -} - -impl From<&str> for AppUrl { - fn from(s: &str) -> AppUrl { - AppUrl { - chunks: s.split("/").map(|chunk| chunk.into()).collect(), - } - } -} - -impl From for AppUrl { - fn from(s: String) -> AppUrl { - s.as_str().into() - } -} - -impl> From> for AppUrl { - fn from(v: Vec) -> AppUrl { - AppUrl { - chunks: v.into_iter().map(|p| p.into()).collect(), - } - } -} - -impl std::ops::Add for AppUrl { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self { - chunks: self - .chunks - .iter() - .chain(other.chunks.iter()) - .cloned() - .collect(), - } - } -} diff --git a/typhon-webapp/src/drv_log.rs b/typhon-webapp/src/drv_log.rs index e6829015..3aed0fd3 100644 --- a/typhon-webapp/src/drv_log.rs +++ b/typhon-webapp/src/drv_log.rs @@ -21,7 +21,7 @@ pub fn init(_orders: &mut impl Orders) -> Model { pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { use crate::get_token; use crate::streams; - use crate::SETTINGS; + use crate::Settings; use gloo_net::http; @@ -31,10 +31,9 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { model.drv = Some(drv.clone()); model.log = Vec::new(); - let settings = SETTINGS.get().unwrap(); - let req = - http::RequestBuilder::new(&format!("{}/drv-log{}", settings.api_server.url(), drv)) - .method(http::Method::GET); + let settings = Settings::load(); + let req = http::RequestBuilder::new(&format!("{}/drv-log{}", settings.url(), drv)) + .method(http::Method::GET); let req = match get_token() { None => req, Some(token) => req.header(&"token", &token), diff --git a/typhon-webapp/src/evaluation.rs b/typhon-webapp/src/evaluation.rs index f104026e..a8f46f29 100644 --- a/typhon-webapp/src/evaluation.rs +++ b/typhon-webapp/src/evaluation.rs @@ -1,18 +1,16 @@ -use crate::{appurl::AppUrl, perform_request, view_error}; +use crate::perform_request; +use crate::view_error; use seed::{prelude::*, *}; use typhon_types::*; +struct_urls!(); + pub struct Model { error: Option, handle: handles::Evaluation, info: Option, log: Option, -} - -impl Model { - pub fn app_url(&self) -> AppUrl { - Vec::::from(self.handle.clone()).into() - } + base_url: Url, } #[derive(Clone, Debug)] @@ -28,13 +26,14 @@ pub enum Msg { Noop, } -pub fn init(orders: &mut impl Orders, handle: handles::Evaluation) -> Model { +pub fn init(base_url: Url, orders: &mut impl Orders, handle: handles::Evaluation) -> Model { orders.send_msg(Msg::FetchInfo); Model { error: None, handle: handle.clone(), info: None, log: None, + base_url, } } @@ -93,6 +92,8 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } fn view_evaluation(model: &Model) -> Node { + let urls_1 = crate::Urls::new(model.base_url.clone()); + let urls_2 = crate::Urls::new(model.base_url.clone()); div![ h2![ "Evaluation", @@ -100,14 +101,14 @@ fn view_evaluation(model: &Model) -> Node { a![ &model.handle.jobset.project.name, attrs! { - At::Href => crate::Urls::project(&model.handle.jobset.project), + At::Href => urls_1.project(&model.handle.jobset.project), }, ], ":", a![ &model.handle.jobset.name, attrs! { - At::Href => crate::Urls::jobset(&model.handle.jobset), + At::Href => urls_2.jobset(&model.handle.jobset), }, ], ":", @@ -125,19 +126,22 @@ fn view_evaluation(model: &Model) -> Node { if info.status == "success" { div![ h3!["Jobs"], - ul![info.jobs.iter().map(|job| li![a![ - job.system.clone(), - " / ", - job.name.clone(), - attrs! { - At::Href => crate::Urls::job( - &handles::Job { - evaluation: model.handle.clone(), - system: job.system.clone(), - name: job.name.clone(), - }) - } - ]])] + ul![info.jobs.iter().map(|job| { + let urls = crate::Urls::new(model.base_url.clone()); + li![a![ + job.system.clone(), + " / ", + job.name.clone(), + attrs! { + At::Href => urls.job( + &handles::Job { + evaluation: model.handle.clone(), + system: job.system.clone(), + name: job.name.clone(), + }) + } + ]] + })] ] } else { empty![] @@ -162,7 +166,7 @@ pub fn view(model: &Model, admin: bool) -> Node { model .error .as_ref() - .map(|err| view_error(err, Msg::ErrorIgnored)) + .map(|err| view_error(&model.base_url, err, Msg::ErrorIgnored)) .unwrap_or(div![ view_evaluation(model), if admin { view_admin() } else { empty![] }, diff --git a/typhon-webapp/src/home.rs b/typhon-webapp/src/home.rs index 64612858..bc8d24ad 100644 --- a/typhon-webapp/src/home.rs +++ b/typhon-webapp/src/home.rs @@ -1,18 +1,16 @@ -use crate::{appurl::AppUrl, perform_request, view_error}; +use crate::perform_request; +use crate::view_error; use seed::{prelude::*, *}; use typhon_types::*; +struct_urls!(); + #[derive(Clone, Default)] pub struct Model { error: Option, projects: Vec<(String, responses::ProjectMetadata)>, new_project: (String, String, bool), -} - -impl From for AppUrl { - fn from(_: Model) -> AppUrl { - AppUrl::default() - } + base_url: Url, } #[derive(Debug, Clone)] @@ -29,9 +27,14 @@ pub enum Msg { UpdateNewProjectFlake, } -pub fn init(orders: &mut impl Orders) -> Model { +pub fn init(base_url: Url, orders: &mut impl Orders) -> Model { orders.send_msg(Msg::FetchProjects); - Model::default() + Model { + error: None, + projects: Vec::new(), + new_project: ("".to_string(), "".to_string(), false), + base_url, + } } pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { @@ -91,14 +94,17 @@ fn view_home(model: &Model, admin: bool) -> Node { h2!["Projects"], table![ tr![th!["Id"], th!["Name"], th!["Description"],], - model.projects.iter().map(|(name, meta)| tr![ - td![a![ - name, - attrs! { At::Href => crate::Urls::project(&handles::project(name.into())) } - ]], - td![String::from(meta.title.clone())], - td![String::from(meta.description.clone())], - ]) + model.projects.iter().map(|(name, meta)| { + let urls = crate::Urls::new(model.base_url.clone()); + tr![ + td![a![ + name, + attrs! { At::Href => urls.project(&handles::project(name.into())) } + ]], + td![String::from(meta.title.clone())], + td![String::from(meta.description.clone())], + ] + }) ], admin.then(|| { let empty = model.new_project == <_>::default(); @@ -157,6 +163,6 @@ pub fn view(model: &Model, admin: bool) -> Node { model .error .as_ref() - .map(|err| view_error(err, Msg::ErrorIgnored)) + .map(|err| view_error(&model.base_url, err, Msg::ErrorIgnored)) .unwrap_or_else(|| view_home(model, admin)) } diff --git a/typhon-webapp/src/job.rs b/typhon-webapp/src/job.rs index 1af34644..cea77331 100644 --- a/typhon-webapp/src/job.rs +++ b/typhon-webapp/src/job.rs @@ -1,10 +1,15 @@ use crate::drv_log; -use crate::{appurl::AppUrl, perform_request, view_error, view_log, SETTINGS}; +use crate::perform_request; +use crate::view_error; +use crate::view_log; +use crate::Settings; use typhon_types::*; use seed::{prelude::*, *}; +struct_urls!(); + pub struct Model { error: Option, handle: handles::Job, @@ -12,12 +17,7 @@ pub struct Model { log_begin: Option, log_end: Option, log: drv_log::Model, -} - -impl Model { - pub fn app_url(&self) -> AppUrl { - Vec::::from(self.handle.clone()).into() - } + base_url: Url, } #[derive(Clone, Debug)] @@ -36,7 +36,7 @@ pub enum Msg { Noop, } -pub fn init(orders: &mut impl Orders, handle: handles::Job) -> Model { +pub fn init(base_url: Url, orders: &mut impl Orders, handle: handles::Job) -> Model { orders.send_msg(Msg::FetchInfo); Model { error: None, @@ -45,6 +45,7 @@ pub fn init(orders: &mut impl Orders, handle: handles::Job) -> Model { log_begin: None, log_end: None, log: drv_log::init(&mut orders.proxy(Msg::LogMsg)), + base_url, } } @@ -122,6 +123,9 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } fn view_job(model: &Model) -> Node { + let urls_1 = crate::Urls::new(model.base_url.clone()); + let urls_2 = crate::Urls::new(model.base_url.clone()); + let urls_3 = crate::Urls::new(model.base_url.clone()); div![ h2![ "Job", @@ -129,21 +133,21 @@ fn view_job(model: &Model) -> Node { a![ &model.handle.evaluation.jobset.project.name, attrs! { - At::Href => crate::Urls::project(&model.handle.evaluation.jobset.project), + At::Href => urls_1.project(&model.handle.evaluation.jobset.project), }, ], ":", a![ &model.handle.evaluation.jobset.name, attrs! { - At::Href => crate::Urls::jobset(&model.handle.evaluation.jobset), + At::Href => urls_2.jobset(&model.handle.evaluation.jobset), }, ], ":", a![ &model.handle.evaluation.num, attrs! { - At::Href => crate::Urls::evaluation(&model.handle.evaluation) + At::Href => urls_3.evaluation(&model.handle.evaluation) }, ], ":", @@ -159,7 +163,7 @@ fn view_job(model: &Model) -> Node { p![format!("Status (build): {}", info.build_status)], p![format!("Status (end): {}", info.end_status)], if info.dist { - let api_url = SETTINGS.get().unwrap().api_server.url(); + let api_url = Settings::load().url(); let job = &model.handle.name; let system = &model.handle.system; let evaluation = &model.handle.evaluation.num; @@ -200,7 +204,7 @@ pub fn view(model: &Model, admin: bool) -> Node { model .error .as_ref() - .map(|err| view_error(err, Msg::ErrorIgnored)) + .map(|err| view_error(&model.base_url, err, Msg::ErrorIgnored)) .unwrap_or(div![ view_job(model), if admin { view_admin() } else { empty![] }, diff --git a/typhon-webapp/src/jobset.rs b/typhon-webapp/src/jobset.rs index 7aa5c71a..6c00a5d8 100644 --- a/typhon-webapp/src/jobset.rs +++ b/typhon-webapp/src/jobset.rs @@ -1,20 +1,18 @@ +use crate::perform_request; use crate::timestamp; -use crate::{appurl::AppUrl, perform_request, view_error}; +use crate::view_error; use seed::{prelude::*, *}; use typhon_types::*; +struct_urls!(); + pub struct Model { error: Option, evaluations: Vec<(i64, timestamp::Model)>, handle: handles::Jobset, info: Option, -} - -impl Model { - pub fn app_url(&self) -> AppUrl { - Vec::::from(self.handle.clone()).into() - } + base_url: Url, } #[derive(Clone, Debug)] @@ -31,7 +29,7 @@ pub enum Msg { TimestampMsg(i64, timestamp::Msg), } -pub fn init(orders: &mut impl Orders, handle: handles::Jobset) -> Model { +pub fn init(base_url: Url, orders: &mut impl Orders, handle: handles::Jobset) -> Model { orders.send_msg(Msg::FetchEvaluations); orders.send_msg(Msg::FetchInfo); Model { @@ -39,6 +37,7 @@ pub fn init(orders: &mut impl Orders, handle: handles::Jobset) -> Model { evaluations: Vec::new(), handle: handle.clone(), info: None, + base_url, } } @@ -126,6 +125,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } fn view_jobset(model: &Model) -> Node { + let urls = crate::Urls::new(model.base_url.clone()); div![ h2![ "Jobset", @@ -133,7 +133,7 @@ fn view_jobset(model: &Model) -> Node { a![ &model.handle.project.name, attrs! { - At::Href => crate::Urls::project(&model.handle.project), + At::Href => urls.project(&model.handle.project), }, ], ":", @@ -144,18 +144,21 @@ fn view_jobset(model: &Model) -> Node { Some(info) => div![div![ format!("Flake: {}", info.url), h3!["Evaluations"], - ul![model.evaluations.iter().map(|(num, time)| li![a![ - timestamp::view(time).map_msg({ - let num = num.clone(); - move |msg| Msg::TimestampMsg(num, msg) - }), - attrs! { At::Href => crate::Urls::evaluation( - &handles::Evaluation{ - jobset: model.handle.clone(), - num: *num, - } - ) }, - ]]),] + ul![model.evaluations.iter().map(|(num, time)| { + let urls = crate::Urls::new(model.base_url.clone()); + li![a![ + timestamp::view(time).map_msg({ + let num = num.clone(); + move |msg| Msg::TimestampMsg(num, msg) + }), + attrs! { At::Href => urls.evaluation( + &handles::Evaluation{ + jobset: model.handle.clone(), + num: *num, + } + )}, + ]] + }),] ]], }, ] @@ -176,7 +179,7 @@ pub fn view(model: &Model, admin: bool) -> Node { model .error .as_ref() - .map(|err| view_error(err, Msg::ErrorIgnored)) + .map(|err| view_error(&model.base_url, err, Msg::ErrorIgnored)) .unwrap_or(div![ view_jobset(model), if admin { view_admin() } else { empty![] }, diff --git a/typhon-webapp/src/lib.rs b/typhon-webapp/src/lib.rs index 13603a01..f711ce88 100644 --- a/typhon-webapp/src/lib.rs +++ b/typhon-webapp/src/lib.rs @@ -1,4 +1,3 @@ -mod appurl; mod drv_log; mod editable_text; mod evaluation; @@ -10,23 +9,21 @@ mod project; mod streams; mod timestamp; -use appurl::AppUrl; use gloo_console::log; use gloo_net::http; use gloo_storage::LocalStorage; use gloo_storage::Storage; -use once_cell::sync::OnceCell; use seed::{prelude::*, *}; use serde::{Deserialize, Serialize}; use typhon_types::*; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct ApiServerSettings { +pub struct Settings { pub baseurl: String, pub https: bool, } -impl ApiServerSettings { +impl Settings { pub fn url(&self) -> String { format!( "{}://{}", @@ -34,34 +31,29 @@ impl ApiServerSettings { self.baseurl, ) } -} -impl Default for ApiServerSettings { - fn default() -> Self { - Self { - baseurl: "127.0.0.1:8000/api".into(), - https: false, - } + pub fn load() -> Self { + serde_json::from_str::>( + &document() + .query_selector("script[id='settings']") + .unwrap() + .unwrap() + .inner_html(), + ) + .unwrap() + .unwrap_or_default() } } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Settings { - pub client_webroot: String, - pub api_server: ApiServerSettings, -} - impl Default for Settings { fn default() -> Self { Self { - client_webroot: "/".into(), - api_server: ApiServerSettings::default(), + baseurl: "127.0.0.1:8000/api".into(), + https: false, } } } -pub static SETTINGS: OnceCell = OnceCell::new(); - pub fn get_token() -> Option { LocalStorage::get("typhon_token").ok() } @@ -77,9 +69,9 @@ pub fn reset_token() { pub async fn handle_request( request: &requests::Request, ) -> Result { - let settings = SETTINGS.get().unwrap(); + let settings = Settings::load(); let token = get_token(); - let req = http::RequestBuilder::new(&settings.api_server.url()).method(http::Method::POST); + let req = http::RequestBuilder::new(&settings.url()).method(http::Method::POST); let req = match token { None => req, Some(token) => req.header("token", &token), @@ -126,41 +118,66 @@ macro_rules! perform_request { pub(crate) use perform_request; -pub fn webroot_chunks() -> impl Iterator { - SETTINGS - .get() - .unwrap() - .client_webroot - .split('/') - .filter(|chunk| !chunk.is_empty()) -} - struct_urls!(); impl<'a> Urls<'a> { - pub fn webroot() -> Url { - Url::new().set_path(webroot_chunks()) + pub fn home_urls(self) -> home::Urls<'a> { + home::Urls::new(self.base_url()) + } + pub fn home(self) -> Url { + self.home_urls().base_url() } - pub fn login() -> Url { - Urls::webroot().add_path_part("login") + pub fn project_urls(self, project: &handles::Project) -> project::Urls<'a> { + project::Urls::new( + self.base_url() + .add_path_part("projects") + .add_path_part(project.name.clone()), + ) } - pub fn home() -> Url { - Urls::webroot() + pub fn project(self, project: &handles::Project) -> Url { + self.project_urls(project).base_url() } - pub fn project(handle: &handles::Project) -> Url { - Urls::webroot() - .add_path_part("projects") - .add_path_part(&handle.name) + pub fn jobset_urls(self, jobset: &handles::Jobset) -> jobset::Urls<'a> { + jobset::Urls::new( + self.base_url() + .add_path_part("projects") + .add_path_part(jobset.project.name.clone()) + .add_path_part("jobsets") + .add_path_part(jobset.name.clone()), + ) } - pub fn jobset(handle: &handles::Jobset) -> Url { - Urls::project(&handle.project).add_path_part(&handle.name) + pub fn jobset(self, jobset: &handles::Jobset) -> Url { + self.jobset_urls(jobset).base_url() } - pub fn evaluation(handle: &handles::Evaluation) -> Url { - Urls::jobset(&handle.jobset).add_path_part(format!("{}", handle.num)) + pub fn evaluation_urls(self, evaluation: &handles::Evaluation) -> evaluation::Urls<'a> { + evaluation::Urls::new( + self.base_url() + .add_path_part("projects") + .add_path_part(evaluation.jobset.project.name.clone()) + .add_path_part("jobsets") + .add_path_part(evaluation.jobset.name.clone()) + .add_path_part("evaluations") + .add_path_part(evaluation.num.to_string()), + ) } - pub fn job(handle: &handles::Job) -> Url { - Urls::evaluation(&handle.evaluation) - .add_path_part(&handle.system) - .add_path_part(&handle.name) + pub fn evaluation(self, evaluation: &handles::Evaluation) -> Url { + self.evaluation_urls(evaluation).base_url() + } + pub fn job_urls(self, job: &handles::Job) -> job::Urls<'a> { + job::Urls::new( + self.base_url() + .add_path_part("projects") + .add_path_part(job.evaluation.jobset.project.name.clone()) + .add_path_part("jobsets") + .add_path_part(job.evaluation.jobset.name.clone()) + .add_path_part("evaluations") + .add_path_part(job.evaluation.num.to_string()) + .add_path_part("jobs") + .add_path_part(job.system.clone()) + .add_path_part(job.name.clone()), + ) + } + pub fn job(self, job: &handles::Job) -> Url { + self.job_urls(job).base_url() } } @@ -175,79 +192,68 @@ pub enum Page { } impl Page { - pub fn app_url(&self) -> AppUrl { - match self { - Page::Login(m) => AppUrl::from("login") + m.app_url(), - Page::Home(_) => AppUrl::default(), - Page::Project(m) => AppUrl::from("projects") + m.app_url(), - Page::Jobset(m) => AppUrl::from("projects") + m.app_url(), - Page::Evaluation(m) => AppUrl::from("projects") + m.app_url(), - Page::Job(m) => AppUrl::from("projects") + m.app_url(), - Page::NotFound => AppUrl::from("404"), - } - } - - fn from_chunks(chunks: Vec<&str>, orders: &mut impl Orders) -> Page { - match chunks.as_slice() { - [] => Page::Home(home::init(&mut orders.proxy(Msg::HomeMsg))), - ["login"] => Page::Login(login::init(&mut orders.proxy(Msg::LoginMsg), None)), + fn init(mut url: Url, orders: &mut impl Orders) -> Self { + let base_url = url.to_base_url(); + let path_parts = url.remaining_path_parts(); + match path_parts.as_slice() { + [] => Page::Home(home::init(base_url, &mut orders.proxy(Msg::HomeMsg))), + ["login"] => Page::Login(login::init( + base_url, + &mut orders.proxy(Msg::LoginMsg), + None, + )), ["projects", project] => Page::Project(project::init( + base_url, &mut orders.proxy(Msg::ProjectMsg), handles::project((*project).into()), )), - ["projects", project, jobset] => Page::Jobset(jobset::init( + ["projects", project, "jobsets", jobset] => Page::Jobset(jobset::init( + base_url, &mut orders.proxy(Msg::JobsetMsg), handles::jobset(((*project).into(), (*jobset).into())), )), - ["projects", project, jobset, evaluation] => evaluation + ["projects", project, "jobsets", jobset, "evaluations", evaluation] => evaluation .parse::() .map(|evaluation| { Page::Evaluation(evaluation::init( + base_url, &mut orders.proxy(Msg::EvaluationMsg), handles::evaluation(((*project).into(), (*jobset).into(), evaluation)), )) }) .unwrap_or(Page::NotFound), - ["projects", project, jobset, evaluation, system, job] => evaluation - .parse::() - .map(|evaluation| { - Page::Job(job::init( - &mut orders.proxy(Msg::JobMsg), - handles::job(( - (*project).into(), - (*jobset).into(), - evaluation, - (*system).into(), - (*job).into(), - )), - )) - }) - .unwrap_or(Page::NotFound), + ["projects", project, "jobsets", jobset, "evaluations", evaluation, "jobs", system, job] => { + evaluation + .parse::() + .map(|evaluation| { + Page::Job(job::init( + base_url, + &mut orders.proxy(Msg::JobMsg), + handles::job(( + (*project).into(), + (*jobset).into(), + evaluation, + (*system).into(), + (*job).into(), + )), + )) + }) + .unwrap_or(Page::NotFound) + } _ => Page::NotFound, } } - - fn init(mut url: Url, orders: &mut impl Orders) -> Self { - let webroot = webroot_chunks().collect::>(); - let path_parts = url.remaining_path_parts(); - Page::from_chunks( - path_parts - .strip_prefix(webroot.as_slice()) - .map(|slice| slice.to_vec()) - .unwrap_or(webroot), - orders, - ) - } } pub struct Model { + base_url: Url, page: Page, admin: bool, events_handle: StreamHandle, } #[derive(Debug)] -enum Msg { +pub enum Msg { HomeMsg(home::Msg), LoginMsg(login::Msg), Login, @@ -260,12 +266,12 @@ enum Msg { EventsReceived(Vec), } -fn init(url: Url, orders: &mut impl Orders) -> Model { +fn init(base_url: Url, orders: &mut impl Orders) -> Model { use futures::stream::StreamExt; orders.subscribe(Msg::UrlChanged); - let settings = SETTINGS.get().unwrap(); - let req = http::RequestBuilder::new(&format!("{}/events", settings.api_server.url())) - .method(http::Method::GET); + let settings = Settings::load(); + let req = + http::RequestBuilder::new(&format!("{}/events", settings.url())).method(http::Method::GET); let req = match get_token() { None => req, Some(token) => req.header(&"token", &token), @@ -284,31 +290,23 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { Msg::EventsReceived(res) })); Model { - page: Page::init(url, orders), + base_url: base_url.clone(), + page: Page::init(base_url, orders), admin: get_token().is_some(), // TODO events_handle, } } -fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { - update_aux(msg, model, orders); - let history = seed::browser::util::history(); - let url = seed::Url::from(model.page.app_url()).to_string(); - let prev = seed::browser::util::window().location().pathname().unwrap(); - if url != prev { - log!(format!("url={url}, prev={prev}")); - let _ = history.push_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&url)); - } -} -fn update_aux(msg: Msg, model: &mut Model, orders: &mut impl Orders) { +pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { match (msg, &mut *model) { (Msg::UrlChanged(subs::UrlChanged(url)), _) => { model.page = Page::init(url, orders); } (Msg::Login, _) => { model.page = Page::Login(login::init( + model.base_url.clone(), &mut orders.proxy(Msg::LoginMsg), - Some(model.page.app_url()), + Some(Url::current()), )) } ( @@ -401,12 +399,17 @@ fn update_aux(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } } -pub fn view_error(err: &responses::ResponseError, msg_ignore: Ms) -> Node { +pub fn view_error( + base_url: &Url, + err: &responses::ResponseError, + msg_ignore: Ms, +) -> Node { + let urls = Urls::new(base_url); div![ h2!["Error"], p![format!("{}", err)], button!["Go back", ev(Ev::Click, |_| msg_ignore)], - a!["Home", attrs! { At::Href => Urls::home() }], + a!["Home", attrs! { At::Href => urls.home() }], ] } @@ -414,7 +417,7 @@ pub fn view_log(log: String) -> Node { tt![log.split('\n').map(|p| { p![p.replace(" ", " ")] })] } -fn header(model: &Model) -> Node { +fn header(url: &Url, model: &Model) -> Node { header![ main![ a![ @@ -431,10 +434,10 @@ fn header(model: &Model) -> Node { "], span!["Typhon"], - attrs! { At::Href => Urls::home() } + attrs! { At::Href => Urls::new(url).home() } ] ], - nav![a!["Home", attrs! { At::Href => Urls::home() }],], + nav![a!["Home", attrs! { At::Href => Urls::new(url).home() }],], if model.admin { button!["Logout", ev(Ev::Click, |_| Msg::Logout)] } else { @@ -451,7 +454,7 @@ fn view(model: &Model) -> impl IntoNodes { raw![" "], - header(model), + header(&model.base_url, model), main![ match &model.page { Page::NotFound => div!["not found!"], @@ -482,8 +485,6 @@ fn view(model: &Model) -> impl IntoNodes { } #[wasm_bindgen] -pub fn app(settings: JsValue) { - let settings = serde_wasm_bindgen::from_value(settings).expect("failed to parse settings"); - SETTINGS.set(settings).unwrap(); +pub fn app() { App::start("app", init, update, view); } diff --git a/typhon-webapp/src/login.rs b/typhon-webapp/src/login.rs index 22b363c8..80966f29 100644 --- a/typhon-webapp/src/login.rs +++ b/typhon-webapp/src/login.rs @@ -1,4 +1,3 @@ -use crate::appurl::AppUrl; use crate::perform_request; use typhon_types::*; @@ -8,14 +7,11 @@ use seed::{prelude::*, *}; pub struct Model { error: bool, password: String, - previous_page_url: Option, + previous_url: Option, + base_url: Url, } -impl Model { - pub fn app_url(&self) -> AppUrl { - AppUrl::default() - } -} +struct_urls!(); #[derive(Clone, Debug)] pub enum Msg { @@ -26,18 +22,20 @@ pub enum Msg { } pub enum OutMsg { - Login(String, AppUrl), + Login(String, Url), } -pub fn init(_orders: &mut impl Orders, previous_page_url: Option) -> Model { +pub fn init(base_url: Url, _orders: &mut impl Orders, previous_url: Option) -> Model { Model { error: false, password: "".into(), - previous_page_url, + previous_url, + base_url, } } pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) -> Option { + let urls = crate::Urls::new(model.base_url.clone()); match msg { Msg::Enter => { let req = requests::Request::Login(model.password.clone()); @@ -51,7 +49,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) -> Opt } Msg::LoggedIn { token } => Some(OutMsg::Login( token, - model.previous_page_url.clone().unwrap_or("".into()), + model.previous_url.clone().unwrap_or(urls.home()), )), Msg::Error => { model.password = "".into(); diff --git a/typhon-webapp/src/main.rs b/typhon-webapp/src/main.rs index bb516c75..c31b9c1f 100644 --- a/typhon-webapp/src/main.rs +++ b/typhon-webapp/src/main.rs @@ -1,56 +1,3 @@ -use typhon_webapp::{app, ApiServerSettings, Settings}; - -use gloo_net::http; - -/// Finds the webroot of the webapp by grapping the [link] tag -/// corresponding to the typhon webapp's wasm. -fn find_webroot() -> Option { - let href = web_sys::window()? - .document()? - .query_selector("link[type='application/wasm']") - .ok() - .flatten()? - .get_attribute("href")?; - web_sys::console::log_1(&format!("href={:#?}", href).into()); - Some(format!( - "{}/", - href.rsplit_once("/") - .map(|(dirname, _)| dirname) - .unwrap_or(".") - )) -} - -async fn load_settings() -> Settings { - let client_webroot = find_webroot().unwrap_or_else(|| { - web_sys::console::warn_1(&"Could not detect webroot".into()); - panic!() - }); - use seed::prelude::*; - let settings_path = format!("{client_webroot}settings.json"); - let req = http::RequestBuilder::new(&settings_path).method(http::Method::GET); - let api_server = match req.send().await { - Ok(data) if data.status() == 0 => data - .json() - .await - .map_err(|_| { - web_sys::console::warn_1( - &format!("Could not load `{}`, using defaults.", settings_path).into(), - ) - }) - .ok(), - _ => None, - } - .unwrap_or_else(ApiServerSettings::default); - Settings { - api_server, - client_webroot, - } -} - -async fn async_main() { - app(serde_wasm_bindgen::to_value(&load_settings().await).unwrap()); -} - pub fn main() { - wasm_bindgen_futures::spawn_local(async_main()) + typhon_webapp::app() } diff --git a/typhon-webapp/src/project.rs b/typhon-webapp/src/project.rs index 60d36950..82c0538e 100644 --- a/typhon-webapp/src/project.rs +++ b/typhon-webapp/src/project.rs @@ -1,22 +1,20 @@ use crate::editable_text; -use crate::{appurl::AppUrl, perform_request, view_error, Urls}; +use crate::perform_request; +use crate::view_error; use seed::{prelude::*, *}; use typhon_types::*; +struct_urls!(); + pub struct Model { error: Option, handle: handles::Project, info: Option, declaration_url: editable_text::Model, declaration_flake: bool, -} - -impl Model { - pub fn app_url(&self) -> AppUrl { - Vec::::from(self.handle.clone()).into() - } + base_url: Url, } #[derive(Clone, Debug)] @@ -34,7 +32,7 @@ pub enum Msg { MsgDeclarationUrl(editable_text::Msg), } -pub fn init(orders: &mut impl Orders, handle: handles::Project) -> Model { +pub fn init(base_url: Url, orders: &mut impl Orders, handle: handles::Project) -> Model { orders.send_msg(Msg::FetchInfo); Model { @@ -43,6 +41,7 @@ pub fn init(orders: &mut impl Orders, handle: handles::Project) -> Model { info: None, declaration_url: editable_text::init("".to_string()), declaration_flake: false, + base_url, } } @@ -284,15 +283,18 @@ fn view_project(model: &Model, is_admin: bool) -> Node { ], div![ h3!["Jobsets"], - ul![info.jobsets.iter().map(|name| li![a![ - name, - attrs! { At::Href => Urls::jobset( - &handles::Jobset { - project: model.handle.clone(), - name: name.into(), - } - ) }, - ]])], + ul![info.jobsets.iter().map(|name| { + let urls = crate::Urls::new(model.base_url.clone()); + li![a![ + name, + attrs! { At::Href => urls.jobset( + &handles::Jobset { + project: model.handle.clone(), + name: name.into(), + } + ) }, + ]] + })], ], ], }, @@ -303,6 +305,6 @@ pub fn view(model: &Model, admin: bool) -> Node { model .error .as_ref() - .map(|err| view_error(err, Msg::ErrorIgnored)) + .map(|err| view_error(&model.base_url, err, Msg::ErrorIgnored)) .unwrap_or(div![view_project(model, admin),]) }