From 71b2f70686ca6b63463467fa3f145120ff4c0897 Mon Sep 17 00:00:00 2001 From: Andrew Rioux Date: Fri, 31 Jan 2025 02:00:22 -0500 Subject: [PATCH] feat: added beacon operation config editor --- Cargo.lock | 12 + sparse-server/Cargo.toml | 2 + .../20250131053917_beacon_config_name.sql | 15 + sparse-server/src/beacons.rs | 21 +- sparse-server/src/beacons/categories.rs | 1 + sparse-server/src/beacons/configs.rs | 296 +++++++++++++++++- sparse-server/src/beacons/listeners.rs | 23 +- sparse-server/style/beacons/_categories.scss | 12 + sparse-server/style/beacons/_configs.scss | 28 ++ sparse-server/style/beacons/_listeners.scss | 2 +- sparse-server/style/main.scss | 6 +- 11 files changed, 398 insertions(+), 20 deletions(-) create mode 100644 sparse-server/migrations/20250131053917_beacon_config_name.sql diff --git a/Cargo.lock b/Cargo.lock index b09152d..032b26c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cron" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" +dependencies = [ + "chrono", + "once_cell", + "winnow", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -2984,6 +2995,7 @@ dependencies = [ "chrono", "codee", "console_error_panic_hook", + "cron", "futures", "futures-util", "hex", diff --git a/sparse-server/Cargo.toml b/sparse-server/Cargo.toml index 250d7f6..b8843e7 100644 --- a/sparse-server/Cargo.toml +++ b/sparse-server/Cargo.toml @@ -40,6 +40,7 @@ hex = { version = "0.4", optional = true } serde = "1.0" cfg-if = "1.0.0" rcgen = { version = "0.13.2", optional = true } +cron = { version = "0.15.0", optional = true } [features] hydrate = ["leptos/hydrate", "chrono/wasmbind"] @@ -62,6 +63,7 @@ ssr = [ "dep:sha2", "dep:hex", "dep:rcgen", + "dep:cron", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", diff --git a/sparse-server/migrations/20250131053917_beacon_config_name.sql b/sparse-server/migrations/20250131053917_beacon_config_name.sql new file mode 100644 index 0000000..a29cf33 --- /dev/null +++ b/sparse-server/migrations/20250131053917_beacon_config_name.sql @@ -0,0 +1,15 @@ +DROP TABLE beacon_config; +CREATE TABLE beacon_config ( + config_id integer PRIMARY KEY AUTOINCREMENT NOT NULL, + config_name varchar NOT NULL, + + mode text check (mode in ('single', 'regular', 'random', 'cron')), + + regular_interval int, + + random_min_time int, + random_max_time int, + + cron_schedule varchar, + cron_mode text check (cron_mode in ('local', 'utc')) +); diff --git a/sparse-server/src/beacons.rs b/sparse-server/src/beacons.rs index a482aa5..8005f74 100644 --- a/sparse-server/src/beacons.rs +++ b/sparse-server/src/beacons.rs @@ -22,9 +22,12 @@ pub struct BeaconResources { add_category: ServerAction, remove_category: ServerAction, rename_category: ServerAction, + add_beacon_config: ServerAction, + remove_beacon_config: ServerAction, listeners: Resource, ServerFnError>>, categories: Resource, ServerFnError>>, + configs: Resource, ServerFnError>> } pub fn provide_beacon_resources() { @@ -37,6 +40,9 @@ pub fn provide_beacon_resources() { let remove_category = ServerAction::::new(); let rename_category = ServerAction::::new(); + let add_beacon_config = ServerAction::::new(); + let remove_beacon_config = ServerAction::::new(); + let listeners = Resource::new( move || ( user.get(), @@ -56,15 +62,27 @@ pub fn provide_beacon_resources() { |_| async { categories::get_categories().await } ); + let configs = Resource::new( + move || ( + user.get(), + add_beacon_config.version().get(), + remove_beacon_config.version().get(), + ), + |_| async { configs::get_beacon_configs().await } + ); + provide_context(BeaconResources { add_listener, remove_listener, add_category, remove_category, rename_category, + add_beacon_config, + remove_beacon_config, listeners, - categories + categories, + configs }); } @@ -86,7 +104,6 @@ pub fn BeaconView() -> impl IntoView {
  • "Configs"
  • "Categories"
  • "Templates"
  • -
  • "Instances"
  • "Commands"
  • diff --git a/sparse-server/src/beacons/categories.rs b/sparse-server/src/beacons/categories.rs index 40fa1bf..701087d 100644 --- a/sparse-server/src/beacons/categories.rs +++ b/sparse-server/src/beacons/categories.rs @@ -225,6 +225,7 @@ fn DisplayCategories(categories: Vec) -> impl IntoView { +
    diff --git a/sparse-server/src/beacons/configs.rs b/sparse-server/src/beacons/configs.rs index 87a07d8..15f0cfd 100644 --- a/sparse-server/src/beacons/configs.rs +++ b/sparse-server/src/beacons/configs.rs @@ -1,10 +1,300 @@ -use leptos::prelude::*; +use leptos::{either::Either, prelude::*}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "ssr")] +use { + std::str::FromStr, + + sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}, + leptos::server_fn::error::NoCustomError, + + crate::db::user +}; + +use super::BeaconResources; + +#[derive(Clone, Serialize, Deserialize)] +pub enum BeaconConfigTypes { + Single, + Regular(i64), + Random(i64, i64), + CronSchedule(String, String), +} + +#[cfg(feature = "ssr")] +impl FromRow<'_, SqliteRow> for BeaconConfigTypes { + fn from_row(row: &SqliteRow) -> sqlx::Result { + match row.try_get("mode")? { + "single" => Ok(Self::Single), + "regular" => Ok(Self::Regular(row.try_get("regular_interval")?)), + "random" => Ok(Self::Random( + row.try_get("random_min_time")?, + row.try_get("random_max_time")?, + )), + "cron" => Ok(Self::CronSchedule( + row.try_get("cron_schedule")?, + row.try_get("cron_mode")? + )), + type_name => Err(sqlx::Error::TypeNotFound { + type_name: type_name.to_string(), + }), + } + } +} + +#[cfg_attr(feature = "ssr", derive(FromRow))] +#[derive(Clone, Serialize, Deserialize)] +pub struct BeaconConfig { + config_id: i64, + config_name: String, + #[cfg_attr(feature = "ssr", sqlx(flatten))] + config_type: BeaconConfigTypes, +} + +#[server] +pub async fn get_beacon_configs() -> Result, ServerFnError> { + let user = user::get_auth_session().await?; + + if user.is_none() { + return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + } + + let db = expect_context::(); + + Ok(sqlx::query_as("SELECT * FROM beacon_config") + .fetch_all(&db) + .await?) +} + +#[server] +pub async fn add_beacon_config( + name: String, + mode: String, + regular_interval: i64, + random_min_time: i64, + random_max_time: i64, + cron_schedule: String, + cron_mode: String, +) -> Result<(), ServerFnError> { + let user = user::get_auth_session().await?; + + if user.is_none() { + return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + } + + let db = expect_context::(); + + match &*mode { + "single" => { + sqlx::query!( + "INSERT INTO beacon_config (config_name, mode) VALUES (?, 'single')", + name + ) + .execute(&db) + .await?; + + Ok(()) + }, + "regular" => { + if regular_interval < 1 { + return Err(ServerFnError::::ServerError("Invalid interval provided".to_owned())) + } + + sqlx::query!( + "INSERT INTO beacon_config (config_name, mode, regular_interval) VALUES (?, 'regular', ?)", + name, + regular_interval + ) + .execute(&db) + .await?; + + Ok(()) + }, + "random" => { + if random_min_time < 1 || random_max_time < random_min_time { + return Err(ServerFnError::::ServerError("Invalid random interval provided".to_owned())) + } + + sqlx::query!( + "INSERT INTO beacon_config (config_name, mode, random_min_time, random_max_time) VALUES (?, 'random', ?, ?)", + name, + random_min_time, + random_max_time + ) + .execute(&db) + .await?; + + Ok(()) + }, + "cron" => { + if let Err(e) = cron::Schedule::from_str(&cron_schedule) { + return Err(ServerFnError::::ServerError(format!( + "Could not parse cron expression: {}", + e + ))) + } + + match &*cron_mode { + "local" | "utc" => {}, + other => { + return Err(ServerFnError::::ServerError("Unrecognized timezone specifier for cron".to_string())) + } + } + + sqlx::query!( + "INSERT INTO beacon_config (config_name, mode, cron_schedule, cron_mode) VALUES (?, 'cron', ?, ?)", + name, + cron_schedule, + cron_mode + ) + .execute(&db) + .await?; + + Ok(()) + }, + _ => { + Err(ServerFnError::::ServerError("Invalid mode supplied".to_owned())) + } + } +} + +#[server] +pub async fn remove_beacon_config(id: i64) -> Result<(), ServerFnError> { + unimplemented!() +} #[component] pub fn ConfigsView() -> impl IntoView { - view! { -
    + let BeaconResources { add_beacon_config, configs, .. } = expect_context(); + view! { +
    +

    "Beacon configuration"

    +
    +

    + "Beacon configuration controls the mode of operation of a beacon. Currently, there are 4 modes:" +

    +
      +
    • "Single: When the beacon runs, perform a single callback and exit"
    • +
    • "Regular: After launching, stay launched and perform a callback every X seconds"
    • +
    • "Random: After launching, stay launched and perform a callback a random amount of time every X seconds between the specified minimum and maximum"
    • +
    • + "Cron: Operate on a crontab schedule (as shown " + "here" + ")" +
    • +
    +
    + + +
    + {move || match add_beacon_config.value().get() { + Some(Ok(_)) => Either::Right(()), + None => Either::Right(()), + Some(Err(e)) => Either::Left(view! { +

    "Error creating config:"

    +

    {format!("{e:?}")}

    + }) + }} + "Add beacon configuration" + + + + + + + + + + + + + + +
    + +
    +
    + + "Loading..."

    }> + { move || match configs.get() { + Some(inner) => Either::Right(match inner { + Err(e) => Either::Left(view! { +

    "There was an error loading configs:"

    +

    {format!("error: {}", e)}

    + }), + Ok(cs) => Either::Right(view! { + + }) + }), + None => Either::Left(view! { +

    "Loading..."

    + }) + }} +
    } } + +#[component] +fn DisplayConfigs(configs: Vec) -> impl IntoView { + let BeaconResources { remove_beacon_config, .. } = expect_context(); + + let configs_view = configs + .iter() + .map(|config| view! { +
  • + {config.config_id} + ": " + {config.config_name.clone()} + " (" + {match &config.config_type { + BeaconConfigTypes::Single => { + format!("Single") + }, + BeaconConfigTypes::Regular(int) => { + format!("Regular; every {int} seconds") + }, + BeaconConfigTypes::Random(min, max) => { + format!("Random; regularly between {min} and {max} seconds") + }, + BeaconConfigTypes::CronSchedule(sch, mode) => { + format!("{mode} cron; schedule: {sch}") + } + }} + ") " + +
  • + }) + .collect_view(); + + view! { + {move || match remove_beacon_config.value().get() { + Some(Ok(_)) => Either::Right(()), + None => Either::Right(()), + Some(Err(e)) => Either::Left(view! { +

    "Error deleting config:"

    +

    {format!("{e:?}")}

    + }) + }} +
      + {configs_view} +
    + } +} diff --git a/sparse-server/src/beacons/listeners.rs b/sparse-server/src/beacons/listeners.rs index 11e33a8..0fdbf55 100644 --- a/sparse-server/src/beacons/listeners.rs +++ b/sparse-server/src/beacons/listeners.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "ssr")] use std::net::Ipv4Addr; use leptos::{either::Either, prelude::*}; @@ -196,17 +197,6 @@ fn DisplayListeners(listeners: Vec) -> impl IntoView { {match listener.active { true => Either::Left(view! { "active!" - }), false => Either::Right(view! { }) }} + }) .collect_view(); diff --git a/sparse-server/style/beacons/_categories.scss b/sparse-server/style/beacons/_categories.scss index e69de29..1e3334e 100644 --- a/sparse-server/style/beacons/_categories.scss +++ b/sparse-server/style/beacons/_categories.scss @@ -0,0 +1,12 @@ +main.beacons div.categories { + padding: 10px; + + fieldset { + display: grid; + grid-template-columns: 150px 200px; + + input, label { + margin: 10px; + } + } +} diff --git a/sparse-server/style/beacons/_configs.scss b/sparse-server/style/beacons/_configs.scss index e69de29..bc76898 100644 --- a/sparse-server/style/beacons/_configs.scss +++ b/sparse-server/style/beacons/_configs.scss @@ -0,0 +1,28 @@ +main.beacons div.config { + padding: 10px; + + fieldset { + display: grid; + grid-template-columns: 300px 200px; + + input, label { + margin: 10px; + } + } + + .mode-regular, .mode-random, .mode-cron { + display: none; + } + + select:has(> option[value="regular"]:checked) ~ .mode-regular { + display: block; + } + + select:has(> option[value="random"]:checked) ~ .mode-random { + display: block; + } + + select:has(> option[value="cron"]:checked) ~ .mode-cron { + display: block; + } +} diff --git a/sparse-server/style/beacons/_listeners.scss b/sparse-server/style/beacons/_listeners.scss index 6c8543f..d63b2fe 100644 --- a/sparse-server/style/beacons/_listeners.scss +++ b/sparse-server/style/beacons/_listeners.scss @@ -1,5 +1,5 @@ main.beacons div.listeners { - form { + form, p, h2 { margin: 10px; } diff --git a/sparse-server/style/main.scss b/sparse-server/style/main.scss index b7f1bcf..abfc2ea 100644 --- a/sparse-server/style/main.scss +++ b/sparse-server/style/main.scss @@ -72,11 +72,11 @@ input[type="submit"], input[type="button"], button { background-color: #fff; - border: 1px solid transparent; + border: 1px solid black; cursor: pointer; margin: 5px; - padding: 2px 4px; - border-radius: 2px; + padding: 5px 7px; + border-radius: 4px; box-shadow: #fff7 0 1px 0 inset; &:hover {