feat: added beacon operation config editor
This commit is contained in:
parent
66b59531c5
commit
71b2f70686
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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'))
|
||||
);
|
||||
@ -22,9 +22,12 @@ pub struct BeaconResources {
|
||||
add_category: ServerAction<categories::AddCategory>,
|
||||
remove_category: ServerAction<categories::RemoveCategory>,
|
||||
rename_category: ServerAction<categories::RenameCategory>,
|
||||
add_beacon_config: ServerAction<configs::AddBeaconConfig>,
|
||||
remove_beacon_config: ServerAction<configs::RemoveBeaconConfig>,
|
||||
|
||||
listeners: Resource<Result<Vec<listeners::PubListener>, ServerFnError>>,
|
||||
categories: Resource<Result<Vec<categories::Category>, ServerFnError>>,
|
||||
configs: Resource<Result<Vec<configs::BeaconConfig>, ServerFnError>>
|
||||
}
|
||||
|
||||
pub fn provide_beacon_resources() {
|
||||
@ -37,6 +40,9 @@ pub fn provide_beacon_resources() {
|
||||
let remove_category = ServerAction::<categories::RemoveCategory>::new();
|
||||
let rename_category = ServerAction::<categories::RenameCategory>::new();
|
||||
|
||||
let add_beacon_config = ServerAction::<configs::AddBeaconConfig>::new();
|
||||
let remove_beacon_config = ServerAction::<configs::RemoveBeaconConfig>::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 {
|
||||
<li><A href="/beacons/configs">"Configs"</A></li>
|
||||
<li><A href="/beacons/categories">"Categories"</A></li>
|
||||
<li><A href="/beacons/templates">"Templates"</A></li>
|
||||
<li><A href="/beacons/instances">"Instances"</A></li>
|
||||
<li><A href="/beacons/commands">"Commands"</A></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
@ -225,6 +225,7 @@ fn DisplayCategories(categories: Vec<Category>) -> impl IntoView {
|
||||
<label>"Name"</label>
|
||||
<input name="name" bind:value=target_rename_name />
|
||||
<input type="hidden" name="id" value=move||target_rename_id.get() />
|
||||
<div></div>
|
||||
<input type="submit" value="Submit" disabled=move||rename_category.pending()/>
|
||||
</fieldset>
|
||||
</ActionForm>
|
||||
|
||||
@ -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<Self> {
|
||||
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<Vec<BeaconConfig>, ServerFnError> {
|
||||
let user = user::get_auth_session().await?;
|
||||
|
||||
if user.is_none() {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
|
||||
}
|
||||
|
||||
let db = expect_context::<SqlitePool>();
|
||||
|
||||
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::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
|
||||
}
|
||||
|
||||
let db = expect_context::<SqlitePool>();
|
||||
|
||||
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::<NoCustomError>::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::<NoCustomError>::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::<NoCustomError>::ServerError(format!(
|
||||
"Could not parse cron expression: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
|
||||
match &*cron_mode {
|
||||
"local" | "utc" => {},
|
||||
other => {
|
||||
return Err(ServerFnError::<NoCustomError>::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::<NoCustomError>::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! {
|
||||
<div>
|
||||
let BeaconResources { add_beacon_config, configs, .. } = expect_context();
|
||||
|
||||
view! {
|
||||
<div class="config">
|
||||
<h2>"Beacon configuration"</h2>
|
||||
<div>
|
||||
<p>
|
||||
"Beacon configuration controls the mode of operation of a beacon. Currently, there are 4 modes:"
|
||||
</p>
|
||||
<ul>
|
||||
<li>"Single: When the beacon runs, perform a single callback and exit"</li>
|
||||
<li>"Regular: After launching, stay launched and perform a callback every X seconds"</li>
|
||||
<li>"Random: After launching, stay launched and perform a callback a random amount of time every X seconds between the specified minimum and maximum"</li>
|
||||
<li>
|
||||
"Cron: Operate on a crontab schedule (as shown "
|
||||
<a href="https://docs.rs/cron/0.15.0/cron/">"here"</a>
|
||||
")"
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ActionForm action=add_beacon_config>
|
||||
<fieldset>
|
||||
{move || match add_beacon_config.value().get() {
|
||||
Some(Ok(_)) => Either::Right(()),
|
||||
None => Either::Right(()),
|
||||
Some(Err(e)) => Either::Left(view! {
|
||||
<p>"Error creating config:"</p>
|
||||
<p>{format!("{e:?}")}</p>
|
||||
})
|
||||
}}
|
||||
<legend>"Add beacon configuration"</legend>
|
||||
<label>"Configuration name"</label>
|
||||
<input name="name" />
|
||||
<label>"Configuration type"</label>
|
||||
<select name="mode">
|
||||
<option value="single">"Single"</option>
|
||||
<option value="regular">"Regular"</option>
|
||||
<option value="random">"Random"</option>
|
||||
<option value="cron">"Cron"</option>
|
||||
</select>
|
||||
<label class="mode-regular">"Interval"</label>
|
||||
<input class="mode-regular" name="regular_interval" type="number" value="0" />
|
||||
<label class="mode-random">"Random interval (min)"</label>
|
||||
<input class="mode-random" name="random_min_time" type="number" value="0" />
|
||||
<label class="mode-random">"Random interval (max)"</label>
|
||||
<input class="mode-random" name="random_max_time" type="number" value="0" />
|
||||
<label class="mode-cron">"Schedule"</label>
|
||||
<input class="mode-cron" name="cron_schedule" />
|
||||
<label class="mode-cron">"Cron timezone"</label>
|
||||
<select class="mode-cron" name="cron_mode">
|
||||
<option value="local">"Local to target"</option>
|
||||
<option value="utc">"UTC"</option>
|
||||
</select>
|
||||
<div></div>
|
||||
<input type="submit" value="Submit" disabled=move||add_beacon_config.pending() />
|
||||
</fieldset>
|
||||
</ActionForm>
|
||||
|
||||
<Suspense fallback=|| view! { <p>"Loading..."</p> }>
|
||||
{ move || match configs.get() {
|
||||
Some(inner) => Either::Right(match inner {
|
||||
Err(e) => Either::Left(view! {
|
||||
<p>"There was an error loading configs:"</p>
|
||||
<p>{format!("error: {}", e)}</p>
|
||||
}),
|
||||
Ok(cs) => Either::Right(view! {
|
||||
<DisplayConfigs configs=cs />
|
||||
})
|
||||
}),
|
||||
None => Either::Left(view! {
|
||||
<p>"Loading..."</p>
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn DisplayConfigs(configs: Vec<BeaconConfig>) -> impl IntoView {
|
||||
let BeaconResources { remove_beacon_config, .. } = expect_context();
|
||||
|
||||
let configs_view = configs
|
||||
.iter()
|
||||
.map(|config| view! {
|
||||
<li>
|
||||
{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}")
|
||||
}
|
||||
}}
|
||||
") "
|
||||
<button
|
||||
on:click={
|
||||
let id = config.config_id;
|
||||
move |_| {
|
||||
remove_beacon_config.dispatch(RemoveBeaconConfig { id });
|
||||
}
|
||||
}
|
||||
>
|
||||
"delete"
|
||||
</button>
|
||||
</li>
|
||||
})
|
||||
.collect_view();
|
||||
|
||||
view! {
|
||||
{move || match remove_beacon_config.value().get() {
|
||||
Some(Ok(_)) => Either::Right(()),
|
||||
None => Either::Right(()),
|
||||
Some(Err(e)) => Either::Left(view! {
|
||||
<p>"Error deleting config:"</p>
|
||||
<p>{format!("{e:?}")}</p>
|
||||
})
|
||||
}}
|
||||
<ul>
|
||||
{configs_view}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use leptos::{either::Either, prelude::*};
|
||||
@ -196,17 +197,6 @@ fn DisplayListeners(listeners: Vec<PubListener>) -> impl IntoView {
|
||||
{match listener.active {
|
||||
true => Either::Left(view! {
|
||||
<span>"active!"</span>
|
||||
<button
|
||||
on:click={
|
||||
let id = listener.listener_id;
|
||||
move |e| {
|
||||
let _ = e.prevent_default();
|
||||
remove_listener.dispatch(RemoveListener { listener_id: id });
|
||||
}
|
||||
}
|
||||
>
|
||||
"delete"
|
||||
</button>
|
||||
}),
|
||||
false => Either::Right(view! {
|
||||
<button
|
||||
@ -222,6 +212,17 @@ fn DisplayListeners(listeners: Vec<PubListener>) -> impl IntoView {
|
||||
</button>
|
||||
})
|
||||
}}
|
||||
<button
|
||||
on:click={
|
||||
let id = listener.listener_id;
|
||||
move |e| {
|
||||
let _ = e.prevent_default();
|
||||
remove_listener.dispatch(RemoveListener { listener_id: id });
|
||||
}
|
||||
}
|
||||
>
|
||||
"delete"
|
||||
</button>
|
||||
</li>
|
||||
})
|
||||
.collect_view();
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
main.beacons div.categories {
|
||||
padding: 10px;
|
||||
|
||||
fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 200px;
|
||||
|
||||
input, label {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
main.beacons div.listeners {
|
||||
form {
|
||||
form, p, h2 {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user