From ffa5bb61f8ba83ff1779cd8a809645e5b045f4d9 Mon Sep 17 00:00:00 2001 From: Jacob Morgan <3956959+slackspace-io@users.noreply.github.com> Date: Sun, 21 Apr 2024 08:44:30 +0200 Subject: [PATCH] Bug fixes, implementing settings handling within Dioxus (#13) * 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. * Show next scheduled run on workload page * Initial settings functions broke overall app. Restored functionality with different approach for settings * Add assets directory to Docker build context * Use workload name instead of git directory in insert_workload function. Resolves #12 --- Dockerfile | 1 + assets/style.css | 49 ++++++++++++- src/config.rs | 14 ++-- src/database/client.rs | 2 +- src/services/scheduler.rs | 11 +++ src/site/app.rs | 141 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 209 insertions(+), 9 deletions(-) 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 diff --git a/assets/style.css b/assets/style.css index 5d2967b..2ae194a 100644 --- a/assets/style.css +++ b/assets/style.css @@ -64,9 +64,56 @@ .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 { 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; +} + +.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; + margin: 20px; + order: 1!important; +} 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/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(()), 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 ccd2b06..0cb8552 100644 --- a/src/site/app.rs +++ b/src/site/app.rs @@ -1,8 +1,11 @@ #![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; +use crate::config::{GitopsConfig, Notifications, Settings, System}; use crate::models; use crate::models::models::Workload; @@ -14,8 +17,15 @@ enum Route { Home {}, #[route("/refresh-all")] RefreshAll {}, + #[route("/settings")] + SettingsPage {}, } +#[derive(Debug, Deserialize, Serialize, Clone)] +#[allow(unused)] +pub struct AppSettings { + pub settings: Settings, +} #[derive(PartialEq, Clone,Props)] @@ -32,6 +42,66 @@ 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 { + // NextScheduledTimeCard {} + // + //}, + 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.system.schedule}" } + }, + div { class: "settings-item", + span { class: "settings-item-key", "Data Directory: " }, + 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.system.run_at_startup}" } + } + }, + div { + class: "settings-section", + div { class: "settings-section-header", "Gitops Settings" }, + 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}" } + } + 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)?; @@ -48,6 +118,7 @@ fn Home() -> Element { } else { rsx! { div { class: "workloads-page", + NextScheduledTimeCard {}, for w in workloads.iter() { WorkloadCard{workload: w.clone()} } @@ -104,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 { } } @@ -118,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 { } } @@ -130,10 +203,32 @@ fn WorkloadCard(props: WorkloadCardProps) -> Element { pub fn App() -> Element { println!("App started"); + 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)?; @@ -204,6 +299,52 @@ fn All() -> Element { } +// ... rest of the code ... + +#[component] +fn NextScheduledTimeCard() -> Element { + 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.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}" } + a { href: "/refresh-all", "Click to Run Now" } + } + } + }, + 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> { log::info!("upgrade_workload: {:?}", workload);