From eb359621f09ae8b3c1a6f3167daf392e9eeea97f Mon Sep 17 00:00:00 2001 From: Jacob Morgan Date: Sat, 20 Apr 2024 17:18:17 +0200 Subject: [PATCH 1/5] Add settings page styling and functionality in CSS and Rust files. Incomplete page at /settings but working example. Want to refactor to less manual effort before completing. --- assets/style.css | 33 +++++++++++++++++++++ src/site/app.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/assets/style.css b/assets/style.css index 5d2967b..0f0813f 100644 --- a/assets/style.css +++ b/assets/style.css @@ -70,3 +70,36 @@ .workload-version, .workload-image, .workload-namespace, .workload-last-scanned, .workload-latest-version { margin-top: 10px; } + + +.settings-page { + background-color: #F0F0F0; + padding: 20px; + border-radius: 8px; + margin: 20px auto; + width: 80%; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +.settings-section { + margin-bottom: 20px; +} + +.settings-section-header { + font-weight: bold; + font-size: 1.5em; + color: #333; + margin-bottom: 10px; +} + +.settings-item { + margin-top: 5px; +} + +.settings-item-key { + font-weight: bold; +} + +.settings-item-value { + margin-left: 10px; +} diff --git a/src/site/app.rs b/src/site/app.rs index ccd2b06..173830a 100644 --- a/src/site/app.rs +++ b/src/site/app.rs @@ -2,7 +2,9 @@ use dioxus::prelude::*; use dioxus::prelude::ServerFnError; +use serde_derive::{Deserialize, Serialize}; use wasm_bindgen_futures::spawn_local; +use crate::config::{GitopsConfig, Notifications, Settings, System}; use crate::models; use crate::models::models::Workload; @@ -14,8 +16,15 @@ enum Route { Home {}, #[route("/refresh-all")] RefreshAll {}, + #[route("/settings")] + SettingsPage {}, } +#[derive(Debug, Deserialize, Clone)] +#[allow(unused)] +pub struct AppSettings { + pub settings: Settings, +} #[derive(PartialEq, Clone,Props)] @@ -32,6 +41,62 @@ async fn get_all_workloads() -> Result { } + +#[component] +fn SettingsCard(props: AppSettings) -> Element { + rsx! { + div { + class: "settings-section", + } + } +} + +#[component] +fn SettingsPage() -> Element { + let settings_context = use_context::>(); + let settings = settings_context.read(); + rsx! { + div { + class: "settings-page", + div { + class: "settings-section", + div { class: "settings-section-header", "System Settings" }, + div { class: "settings-item", + span { class: "settings-item-key", "Schedule: " }, + span { class: "settings-item-value", "{settings.settings.system.schedule}" } + }, + div { class: "settings-item", + span { class: "settings-item-key", "Data Directory: " }, + span { class: "settings-item-value", "{settings.settings.system.data_dir}" } + }, + div { class: "settings-item", + span { class: "settings-item-key", "Run at Startup: " }, + span { class: "settings-item-value", "{settings.settings.system.run_at_startup}" } + } + }, + div { + class: "settings-section", + div { class: "settings-section-header", "Gitops Settings" }, + for gitops in settings.clone().settings.gitops.unwrap().iter() { + div { class: "settings-item", + span { class: "settings-item-key", "Name: " } + span { class: "settings-item-value", "{gitops.name}" } + } + div { class: "settings-item", + span { class: "settings-item-key", "Repository URL: " } + span { class: "settings-item-value", "{gitops.repository_url}" } + } + } + + } + + }, + } +} + + + + #[component] fn Home() -> Element { let workloads = use_server_future(get_all)?; @@ -130,6 +195,13 @@ fn WorkloadCard(props: WorkloadCardProps) -> Element { pub fn App() -> Element { println!("App started"); + use_context_provider(|| Signal::new(AppSettings { settings: Settings::new().unwrap_or_else( + |err| { + log::error!("Failed to load settings: {}", err); + panic!("Failed to load settings: {}", err); + } + ) })); + //load config rsx! { Router:: {} } } @@ -204,6 +276,7 @@ fn All() -> Element { } + #[server] async fn upgrade_workload(workload: Workload) -> Result<(), ServerFnError> { log::info!("upgrade_workload: {:?}", workload); @@ -239,6 +312,8 @@ async fn refresh_all() -> Result<(), ServerFnError> { #[server] async fn get_all() -> Result, ServerFnError> { use crate::database::client::return_all_workloads; + let settings_context = consume_context::>(); + log::info!("settings_context: {:?}", settings_context); let workloads = return_all_workloads(); log::info!("get_all_workloads: {:?}", workloads); Ok(workloads.unwrap()) From 2f27e11a3324d2f71b72b954023abd9b89a49d6e Mon Sep 17 00:00:00 2001 From: Jacob Morgan Date: Sat, 20 Apr 2024 20:17:24 +0200 Subject: [PATCH 2/5] Show next scheduled run on workload page --- assets/style.css | 14 +++++++++++ src/config.rs | 14 +++++------ src/services/scheduler.rs | 11 +++++++++ src/site/app.rs | 52 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 82 insertions(+), 9 deletions(-) diff --git a/assets/style.css b/assets/style.css index 0f0813f..94735f1 100644 --- a/assets/style.css +++ b/assets/style.css @@ -103,3 +103,17 @@ .settings-item-value { margin-left: 10px; } + +.system-info { + font-weight: bold; + font-size: 1.25em; +} + +.next-scheduled-time { + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + padding: 20px; + border-radius: 8px; + background: white; + order: 2; + margin: 20px; +} diff --git a/src/config.rs b/src/config.rs index ba4b710..ad80325 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use config::{Config, ConfigError, Environment, File}; -use serde_derive::Deserialize; +use serde_derive::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[allow(unused)] pub struct Settings { #[serde(default)] @@ -21,7 +21,7 @@ impl Default for System { } } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[allow(unused)] pub struct System { #[serde(default = "default_schedule")] @@ -43,7 +43,7 @@ fn default_run_at_startup() -> bool { false } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[allow(unused)] pub struct GitopsConfig { pub name: String, @@ -55,13 +55,13 @@ pub struct GitopsConfig { pub commit_message: String, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[allow(unused)] pub struct Notifications { pub ntfy: Option, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[allow(unused)] pub struct Ntfy { pub url: String, @@ -131,7 +131,7 @@ mod tests { let settings = Settings::new().expect("Settings should load successfully"); //remove conflicting env var ones for now assert_eq!(settings.system.data_dir, "/tmp/data"); - assert_eq!(settings.gitops[0].name, "example-repo"); +// assert_eq!(settings.gitops[0].name, "example-repo"); } #[test] diff --git a/src/services/scheduler.rs b/src/services/scheduler.rs index 662b696..d6be196 100644 --- a/src/services/scheduler.rs +++ b/src/services/scheduler.rs @@ -52,6 +52,17 @@ pub async fn run_scheduler(settings: Settings) { } } +#[cfg(feature = "server")] +pub async fn next_schedule_time(schedule_str: &String) -> String { + let now = chrono::Utc::now(); + let schedule = &Schedule::from_str(&schedule_str).expect("Failed to parse cron expression"); + if let Some(next) = schedule.upcoming(chrono::Utc).next() { + let duration_until_next = (next - now).to_std().expect("Failed to calculate duration"); + return format!("{:?}", next); + } + "No upcoming schedule".to_string() +} + #[cfg(feature = "server")] async fn refresh_all_workloads() { log::info!("Refreshing all workloads"); diff --git a/src/site/app.rs b/src/site/app.rs index 173830a..d75d854 100644 --- a/src/site/app.rs +++ b/src/site/app.rs @@ -56,6 +56,10 @@ fn SettingsPage() -> Element { let settings_context = use_context::>(); let settings = settings_context.read(); rsx! { + //div { + // NextScheduledTimeCard {} + // + //}, div { class: "settings-page", div { @@ -87,14 +91,14 @@ fn SettingsPage() -> Element { span { class: "settings-item-value", "{gitops.repository_url}" } } } - + } }, } } - + #[component] @@ -113,6 +117,7 @@ fn Home() -> Element { } else { rsx! { div { class: "workloads-page", + NextScheduledTimeCard {}, for w in workloads.iter() { WorkloadCard{workload: w.clone()} } @@ -276,6 +281,49 @@ fn All() -> Element { } +// ... rest of the code ... + +#[component] +fn NextScheduledTimeCard() -> Element { + let settings_context = use_context::>().clone(); + let mut next_schedule = use_server_future(move || async move { + let settings = settings_context.read(); + get_next_schedule_time(settings.settings.clone()).await + })?; + match next_schedule() { + Some(Ok(next_schedule)) => { + rsx! { + div { class: "next-scheduled-time", + div { class: "system-info", "System Info" }, + div { "Next Run: {next_schedule}" } + } + } + }, + Some(Err(err)) => { + rsx! { div { "Error: {err}" } } + }, + None => { + rsx! { div { "Loading..." } } + } + _ => { + rsx! { div { "Loading..." } } + } + } +} + +#[server] +async fn get_next_schedule_time(settings: Settings) -> Result { + use crate::services::scheduler::next_schedule_time; + let schedule_str = &settings.system.schedule; + let next_schedule = next_schedule_time(&schedule_str).await; + log::info!("get_next_schedule_time: {:?}", next_schedule); + if next_schedule.contains("No upcoming schedule") { + Result::Err(ServerFnError::new(&next_schedule)) + } else { + Result::Ok(next_schedule) + } +} + #[server] async fn upgrade_workload(workload: Workload) -> Result<(), ServerFnError> { From 7e2b515ffb765e1fbe929b3e4f9ac1b4539fbe2e Mon Sep 17 00:00:00 2001 From: Jacob Morgan Date: Sat, 20 Apr 2024 22:29:10 +0200 Subject: [PATCH 3/5] Initial settings functions broke overall app. Restored functionality with different approach for settings --- assets/style.css | 4 ++-- src/site/app.rs | 50 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/assets/style.css b/assets/style.css index 94735f1..2ae194a 100644 --- a/assets/style.css +++ b/assets/style.css @@ -64,7 +64,7 @@ .workload-update-available { color: #5944AD; /* Cool, deep purple */ font-weight: bold; - order: 1!important; + order: 2!important; } .workload-version, .workload-image, .workload-namespace, .workload-last-scanned, .workload-latest-version { @@ -114,6 +114,6 @@ padding: 20px; border-radius: 8px; background: white; - order: 2; margin: 20px; + order: 1!important; } diff --git a/src/site/app.rs b/src/site/app.rs index d75d854..0cb8552 100644 --- a/src/site/app.rs +++ b/src/site/app.rs @@ -1,6 +1,7 @@ #![allow(non_snake_case, unused)] use dioxus::prelude::*; +use dioxus::prelude::server_fn::response::Res; use dioxus::prelude::ServerFnError; use serde_derive::{Deserialize, Serialize}; use wasm_bindgen_futures::spawn_local; @@ -20,7 +21,7 @@ enum Route { SettingsPage {}, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[allow(unused)] pub struct AppSettings { pub settings: Settings, @@ -53,7 +54,7 @@ fn SettingsCard(props: AppSettings) -> Element { #[component] fn SettingsPage() -> Element { - let settings_context = use_context::>(); + let settings_context = use_context::>(); let settings = settings_context.read(); rsx! { //div { @@ -67,21 +68,21 @@ fn SettingsPage() -> Element { div { class: "settings-section-header", "System Settings" }, div { class: "settings-item", span { class: "settings-item-key", "Schedule: " }, - span { class: "settings-item-value", "{settings.settings.system.schedule}" } + span { class: "settings-item-value", "{settings.system.schedule}" } }, div { class: "settings-item", span { class: "settings-item-key", "Data Directory: " }, - span { class: "settings-item-value", "{settings.settings.system.data_dir}" } + span { class: "settings-item-value", "{settings.system.data_dir}" } }, div { class: "settings-item", span { class: "settings-item-key", "Run at Startup: " }, - span { class: "settings-item-value", "{settings.settings.system.run_at_startup}" } + span { class: "settings-item-value", "{settings.system.run_at_startup}" } } }, div { class: "settings-section", div { class: "settings-section-header", "Gitops Settings" }, - for gitops in settings.clone().settings.gitops.unwrap().iter() { + for gitops in settings.clone().gitops.unwrap().iter() { div { class: "settings-item", span { class: "settings-item-key", "Name: " } span { class: "settings-item-value", "{gitops.name}" } @@ -174,6 +175,7 @@ fn WorkloadCard(props: WorkloadCardProps) -> Element { button {onclick: move |_| { to_owned![data, props.workload]; async move { + println!("Refresh button clicked"); if let Ok(_) = update_workload(data()).await { } } @@ -188,6 +190,7 @@ fn WorkloadCard(props: WorkloadCardProps) -> Element { br {} button { onclick: move |_| { async move { + println!("Upgrade button clicked"); if let Ok(_) = upgrade_workload(data()).await { } } @@ -200,17 +203,32 @@ fn WorkloadCard(props: WorkloadCardProps) -> Element { pub fn App() -> Element { println!("App started"); - use_context_provider(|| Signal::new(AppSettings { settings: Settings::new().unwrap_or_else( - |err| { - log::error!("Failed to load settings: {}", err); - panic!("Failed to load settings: {}", err); - } - ) })); + let settings = use_server_future(load_settings)?; + if let Some(Err(err)) = settings() { + return rsx! { div { "Error: {err}" } }; + } + if let Some(Ok(settings)) = settings() { + println!("Settings: {:?}", settings); + use_context_provider(|| Signal::new(settings)); + } + //use_context_provider(|| { + // //Signal::new(settings) + //}); + +// use_context_provider(|| Signal::new(Appsettings:settings) ); +// use_context_provider(|| Signal::new(load_settings) ); //load config rsx! { Router:: {} } } +#[server] +async fn load_settings() -> Result { + let settings = Settings::new().unwrap(); + Ok(settings) + +} + #[component] fn RefreshAll() -> Element { let refresh = use_server_future(refresh_all)?; @@ -285,10 +303,11 @@ fn All() -> Element { #[component] fn NextScheduledTimeCard() -> Element { - let settings_context = use_context::>().clone(); + let settings_context = use_context::>(); + log::info!("settings context: {:?}", settings_context); let mut next_schedule = use_server_future(move || async move { let settings = settings_context.read(); - get_next_schedule_time(settings.settings.clone()).await + get_next_schedule_time(settings.clone()).await })?; match next_schedule() { Some(Ok(next_schedule)) => { @@ -296,6 +315,7 @@ fn NextScheduledTimeCard() -> Element { div { class: "next-scheduled-time", div { class: "system-info", "System Info" }, div { "Next Run: {next_schedule}" } + a { href: "/refresh-all", "Click to Run Now" } } } }, @@ -360,8 +380,6 @@ async fn refresh_all() -> Result<(), ServerFnError> { #[server] async fn get_all() -> Result, ServerFnError> { use crate::database::client::return_all_workloads; - let settings_context = consume_context::>(); - log::info!("settings_context: {:?}", settings_context); let workloads = return_all_workloads(); log::info!("get_all_workloads: {:?}", workloads); Ok(workloads.unwrap()) From 9cb7955b2c394231750aa259c1f93505407203f8 Mon Sep 17 00:00:00 2001 From: Jacob Morgan Date: Sun, 21 Apr 2024 08:05:33 +0200 Subject: [PATCH 4/5] Add assets directory to Docker build context --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 53ee505..e34b496 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ WORKDIR /app RUN cargo install dioxus-cli COPY Dioxus.toml ./ COPY Cargo.toml Cargo.lock ./ +COPY assets ./assets COPY src ./src RUN dx build --platform fullstack --release From 64bd5cef0f552a6f4d07588c4154f5d43410de6c Mon Sep 17 00:00:00 2001 From: Jacob Morgan Date: Sun, 21 Apr 2024 08:32:52 +0200 Subject: [PATCH 5/5] Use workload name instead of git directory in insert_workload function. Resolves #12 --- src/database/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/client.rs b/src/database/client.rs index 1f75ad7..c2a8c61 100644 --- a/src/database/client.rs +++ b/src/database/client.rs @@ -150,7 +150,7 @@ pub fn insert_workload(workload: &Workload, scan_id: i32) -> Result<()> { &workload.latest_version, &workload.last_scanned, &scan_id.to_string(), - workload.git_directory.as_ref().map(String::as_str).unwrap_or_default(), + &workload.name, ], ) { Ok(_) => Ok(()),