diff --git a/src/app/data/mod.rs b/src/app/data/mod.rs index a76a2e9..16000ec 100644 --- a/src/app/data/mod.rs +++ b/src/app/data/mod.rs @@ -1,12 +1,11 @@ pub mod source; pub mod store; -use std::path::PathBuf; -use std::sync::{RwLock, Mutex}; -use std::num::ParseFloatError; -use eframe::epaint::Color32; -use self::store::SQLiteDataStore; use self::source::{Panel, Source}; +use self::store::SQLiteDataStore; +use std::num::ParseFloatError; +use std::path::PathBuf; +use std::sync::{Mutex, RwLock}; #[derive(Debug)] pub enum FetchError { @@ -18,20 +17,31 @@ pub enum FetchError { ParseFloatError(ParseFloatError), } -impl From:: for FetchError { - fn from(e: ureq::Error) -> Self { FetchError::UreqError(e) } +impl From for FetchError { + fn from(e: ureq::Error) -> Self { + FetchError::UreqError(e) + } } -impl From:: for FetchError { - fn from(e: std::io::Error) -> Self { FetchError::IoError(e) } +impl From for FetchError { + fn from(e: std::io::Error) -> Self { + FetchError::IoError(e) + } } -impl From:: for FetchError { // TODO wtf? why does JQL error as a String? - fn from(e: String) -> Self { FetchError::JQLError(e) } +impl From for FetchError { + // TODO wtf? why does JQL error as a String? + fn from(e: String) -> Self { + FetchError::JQLError(e) + } } -impl From:: for FetchError { - fn from(e: ParseFloatError) -> Self { FetchError::ParseFloatError(e) } +impl From for FetchError { + fn from(e: ParseFloatError) -> Self { + FetchError::ParseFloatError(e) + } } -impl From:: for FetchError { - fn from(e: rusqlite::Error) -> Self { FetchError::RusqliteError(e) } +impl From for FetchError { + fn from(e: rusqlite::Error) -> Self { + FetchError::RusqliteError(e) + } } pub struct ApplicationState { @@ -45,13 +55,13 @@ pub struct ApplicationState { } impl ApplicationState { - pub fn new(path:PathBuf) -> Result { + pub fn new(path: PathBuf) -> Result { let storage = SQLiteDataStore::new(path.clone())?; let panels = storage.load_panels()?; let sources = storage.load_sources()?; - return Ok(ApplicationState{ + return Ok(ApplicationState { run: true, file_size: RwLock::new(std::fs::metadata(path.clone())?.len()), file_path: path, @@ -63,22 +73,42 @@ impl ApplicationState { } pub fn add_panel(&self, panel: &Panel) -> Result<(), FetchError> { - let verified_panel = self.storage.lock().expect("Storage Mutex poisoned") + let verified_panel = self + .storage + .lock() + .expect("Storage Mutex poisoned") .new_panel( panel.name.as_str(), panel.view_size, panel.width, panel.height, - self.panels.read().expect("Panels RwLock poisoned").len() as i32 // todo can this be made more compact and without acquisition? + self.panels.read().expect("Panels RwLock poisoned").len() as i32, // todo can this be made more compact and without acquisition? )?; // TODO make values customizable and useful - self.panels.write().expect("Panels RwLock poisoned").push(verified_panel); + self.panels + .write() + .expect("Panels RwLock poisoned") + .push(verified_panel); Ok(()) } pub fn add_source(&self, source: &Source) -> Result<(), FetchError> { - let verified_source = self.storage.lock().expect("Storage Mutex poisoned") - .new_source(source.panel_id, source.name.as_str(), source.url.as_str(), source.query_x.as_str(), source.query_y.as_str(), source.color, source.visible)?; - self.sources.write().expect("Sources RwLock poisoned").push(verified_source); + let verified_source = self + .storage + .lock() + .expect("Storage Mutex poisoned") + .new_source( + source.panel_id, + source.name.as_str(), + source.url.as_str(), + source.query_x.as_str(), + source.query_y.as_str(), + source.color, + source.visible, + )?; + self.sources + .write() + .expect("Sources RwLock poisoned") + .push(verified_source); return Ok(()); } } diff --git a/src/app/data/source.rs b/src/app/data/source.rs index 0ad119e..0bd7c67 100644 --- a/src/app/data/source.rs +++ b/src/app/data/source.rs @@ -1,8 +1,8 @@ -use std::sync::RwLock; -use chrono::{DateTime, Utc}; -use eframe::egui::plot::{Values, Value}; -use eframe::epaint::Color32; use super::FetchError; +use chrono::{DateTime, Utc}; +use eframe::egui::plot::{Value, Values}; +use eframe::epaint::Color32; +use std::sync::RwLock; pub struct Panel { pub(crate) id: i32, @@ -59,7 +59,7 @@ impl Default for Source { query_x: "".to_string(), query_y: "".to_string(), panel_id: -1, - data: RwLock::new(Vec::new()) + data: RwLock::new(Vec::new()), } } } @@ -74,21 +74,25 @@ impl Source { Values::from_values(self.data.read().expect("Values RwLock poisoned").clone()) } - pub fn values_filter(&self, min_x:f64) -> Values { + pub fn values_filter(&self, min_x: f64) -> Values { let mut values = self.data.read().expect("Values RwLock poisoned").clone(); values.retain(|x| x.x > min_x); Values::from_values(values) } } -pub fn fetch(url:&str, query_x:&str, query_y:&str) -> Result { +pub fn fetch(url: &str, query_x: &str, query_y: &str) -> Result { let res = ureq::get(url).call()?.into_json()?; - let x : f64; + let x: f64; if query_x.len() > 0 { - x = jql::walker(&res, query_x)?.as_f64().ok_or(FetchError::JQLError("X query is null".to_string()))?; // TODO what if it's given to us as a string? + x = jql::walker(&res, query_x)? + .as_f64() + .ok_or(FetchError::JQLError("X query is null".to_string()))?; // TODO what if it's given to us as a string? } else { x = Utc::now().timestamp() as f64; } - let y = jql::walker(&res, query_y)?.as_f64().ok_or(FetchError::JQLError("Y query is null".to_string()))?; - return Ok( Value { x, y } ); + let y = jql::walker(&res, query_y)? + .as_f64() + .ok_or(FetchError::JQLError("Y query is null".to_string()))?; + return Ok(Value { x, y }); } diff --git a/src/app/data/store.rs b/src/app/data/store.rs index c70c8d8..a24e1c8 100644 --- a/src/app/data/store.rs +++ b/src/app/data/store.rs @@ -1,9 +1,12 @@ -use crate::app::{data::source::{Panel, Source}, util::repack_color}; +use crate::app::util::unpack_color; +use crate::app::{ + data::source::{Panel, Source}, + util::repack_color, +}; use chrono::{TimeZone, Utc}; -use eframe::egui::{Color32, plot::Value}; +use eframe::egui::{plot::Value, Color32}; use rusqlite::{params, Connection}; use std::sync::RwLock; -use crate::app::util::unpack_color; pub trait DataStorage { fn add_panel(&self, name: &str); @@ -60,8 +63,6 @@ impl SQLiteDataStore { Ok(SQLiteDataStore { conn }) } - - pub fn load_values(&self, source_id: i32) -> rusqlite::Result> { let mut values: Vec = Vec::new(); let mut statement = self @@ -90,13 +91,9 @@ impl SQLiteDataStore { ) } - - pub fn load_sources(&self) -> rusqlite::Result> { let mut sources: Vec = Vec::new(); - let mut statement = self - .conn - .prepare("SELECT * FROM sources")?; + let mut statement = self.conn.prepare("SELECT * FROM sources")?; let sources_iter = statement.query_map([], |row| { Ok(Source { id: row.get(0)?, @@ -134,7 +131,11 @@ impl SQLiteDataStore { color: Color32, visible: bool, ) -> rusqlite::Result { - let color_u32 : Option = if color == Color32::TRANSPARENT { None } else { Some(repack_color(color)) }; + let color_u32: Option = if color == Color32::TRANSPARENT { + None + } else { + Some(repack_color(color)) + }; self.conn.execute( "INSERT INTO sources(name, url, interval, query_x, query_y, panel_id, color, visible) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", params![name, url, 60i32, query_x, query_y, panel_id, color_u32, visible], @@ -177,7 +178,11 @@ impl SQLiteDataStore { color: Color32, visible: bool, ) -> rusqlite::Result { - let color_u32 : Option = if color == Color32::TRANSPARENT { None } else { Some(repack_color(color)) }; + let color_u32: Option = if color == Color32::TRANSPARENT { + None + } else { + Some(repack_color(color)) + }; self.conn.execute( "UPDATE sources SET name = ?, url = ?, interval = ?, query_x = ?, query_y = ?, panel_id = ?, color = ?, visible = ? WHERE id = ?", params![name, url, interval, query_x, query_y, panel_id, color_u32, visible, source_id], @@ -190,7 +195,9 @@ impl SQLiteDataStore { pub fn load_panels(&self) -> rusqlite::Result> { let mut panels: Vec = Vec::new(); - let mut statement = self.conn.prepare("SELECT * FROM panels ORDER BY position")?; + let mut statement = self + .conn + .prepare("SELECT * FROM panels ORDER BY position")?; let panels_iter = statement.query_map([], |row| { Ok(Panel { id: row.get(0)?, @@ -214,7 +221,14 @@ impl SQLiteDataStore { } // jank! TODO make it not jank! - pub fn new_panel(&self, name: &str, view_size:i32, width: i32, height: i32, position: i32) -> rusqlite::Result { + pub fn new_panel( + &self, + name: &str, + view_size: i32, + width: i32, + height: i32, + position: i32, + ) -> rusqlite::Result { self.conn.execute( "INSERT INTO panels (name, view_scroll, view_size, timeserie, width, height, limit_view, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", params![name, true, view_size, true, width, height, false, position] @@ -260,7 +274,4 @@ impl SQLiteDataStore { // pub fn delete_panel(&self, id:i32) -> rusqlite::Result { // self.conn.execute("DELETE FROM panels WHERE id = ?", params![id]) // } - - - } diff --git a/src/app/gui/mod.rs b/src/app/gui/mod.rs index 4f2bb82..cf2695c 100644 --- a/src/app/gui/mod.rs +++ b/src/app/gui/mod.rs @@ -1,2 +1,2 @@ -pub mod source; pub mod panel; +pub mod source; diff --git a/src/app/gui/panel.rs b/src/app/gui/panel.rs index 62ca023..9567ff3 100644 --- a/src/app/gui/panel.rs +++ b/src/app/gui/panel.rs @@ -1,7 +1,13 @@ -use chrono::{Utc, Local}; -use eframe::egui::{Ui, Layout, Align, plot::{Plot, Legend, Corner, Line, GridMark}, Slider, DragValue}; +use chrono::{Local, Utc}; +use eframe::egui::{ + plot::{Corner, GridMark, Legend, Line, Plot}, + DragValue, Layout, Slider, Ui, +}; -use crate::app::{data::source::{Panel, Source}, util::timestamp_to_str}; +use crate::app::{ + data::source::{Panel, Source}, + util::timestamp_to_str, +}; pub fn panel_edit_inline_ui(ui: &mut Ui, panel: &mut Panel) { eframe::egui::TextEdit::singleline(&mut panel.name) @@ -27,16 +33,13 @@ pub fn panel_title_ui(ui: &mut Ui, panel: &mut Panel) { ui.separator(); ui.checkbox(&mut panel.timeserie, "timeserie"); ui.separator(); - ui.add( - Slider::new(&mut panel.height, 0..=500).text("height"), - ); + ui.add(Slider::new(&mut panel.height, 0..=500).text("height")); ui.separator(); }); }); }); } - pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec) { let mut p = Plot::new(format!("plot-{}", panel.name)) .height(panel.height as f32) @@ -44,26 +47,20 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec) { .legend(Legend::default().position(Corner::LeftTop)); if panel.view_scroll { - p = p - .include_x(Utc::now().timestamp() as f64); + p = p.include_x(Utc::now().timestamp() as f64); if panel.limit { p = p - .set_margin_fraction(eframe::emath::Vec2{x:0.0, y:0.1}) - .include_x((Utc::now().timestamp() + ( panel.view_size as i64 * 3)) as f64); + .set_margin_fraction(eframe::emath::Vec2 { x: 0.0, y: 0.1 }) + .include_x((Utc::now().timestamp() + (panel.view_size as i64 * 3)) as f64); } if panel.limit { - p = p.include_x( - (Utc::now().timestamp() - (panel.view_size as i64 * 60)) - as f64, - ); + p = p.include_x((Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64); } } if panel.timeserie { p = p - .x_axis_formatter(|x, _range| { - timestamp_to_str(x as i64, true, false) - }) + .x_axis_formatter(|x, _range| timestamp_to_str(x as i64, true, false)) .label_formatter(|name, value| { if !name.is_empty() { return format!( @@ -111,8 +108,7 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec) { if source.visible && source.panel_id == panel.id { let line = if panel.limit { Line::new(source.values_filter( - (Utc::now().timestamp() - - (panel.view_size as i64 * 60)) as f64, + (Utc::now().timestamp() - (panel.view_size as i64 * 60)) as f64, )) .name(source.name.as_str()) } else { diff --git a/src/app/gui/source.rs b/src/app/gui/source.rs index c9db406..724449a 100644 --- a/src/app/gui/source.rs +++ b/src/app/gui/source.rs @@ -1,7 +1,7 @@ use eframe::egui; use eframe::egui::Ui; -use crate::app::data::source::{Source, Panel}; +use crate::app::data::source::{Panel, Source}; pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec) { eframe::egui::TextEdit::singleline(&mut source.name) @@ -25,11 +25,7 @@ pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec) { ui.group(|ui| { ui.horizontal(|ui| { @@ -66,11 +61,7 @@ pub fn source_ui(ui: &mut Ui, source: &mut Source, panels: &Vec) { .width(70.0) .show_ui(ui, |ui| { for p in panels { - ui.selectable_value( - &mut source.panel_id, - p.id, - p.name.as_str(), - ); + ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str()); } }); ui.color_edit_button_srgba(&mut source.color); diff --git a/src/app/mod.rs b/src/app/mod.rs index b736e3b..57f632a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3,21 +3,15 @@ pub mod gui; pub mod util; pub mod worker; -use chrono::{Local, Utc}; use eframe::egui; -use eframe::egui::plot::GridMark; -use eframe::egui::{ - plot::{Line, Plot}, - Color32, -}; use std::sync::Arc; use tracing::error; +use self::data::source::{Panel, Source}; use self::data::ApplicationState; -use self::data::source::{Panel,Source}; -use self::gui::panel::{panel_edit_inline_ui, panel_title_ui, panel_body_ui}; -use self::gui::source::{source_ui, source_edit_inline_ui}; -use self::util::{human_size, timestamp_to_str}; +use self::gui::panel::{panel_body_ui, panel_edit_inline_ui, panel_title_ui}; +use self::gui::source::{source_edit_inline_ui, source_ui}; +use self::util::human_size; use self::worker::native_save; pub struct App { @@ -61,7 +55,11 @@ impl eframe::App for App { } ui.separator(); ui.label("+ source"); - source_edit_inline_ui(ui, &mut self.input_source, &self.data.panels.read().expect("Panels RwLock poisoned")); + source_edit_inline_ui( + ui, + &mut self.input_source, + &self.data.panels.read().expect("Panels RwLock poisoned"), + ); if ui.button("add").clicked() { if let Err(e) = self.data.add_source(&self.input_source) { error!(target: "ui", "Error adding souce : {:?}", e); @@ -154,22 +152,22 @@ impl eframe::App for App { ui.make_persistent_id(format!("panel-{}-compressable", panel.id)), true, ) - .show_header(ui, |ui| { - if self.edit { - if ui.small_button(" + ").clicked() { - if index > 0 { - to_swap.push(index); // TODO kinda jank but is there a better way? - } - } - if ui.small_button(" - ").clicked() { - if index < panels_count - 1 { - to_swap.push(index + 1); // TODO kinda jank but is there a better way? - } + .show_header(ui, |ui| { + if self.edit { + if ui.small_button(" + ").clicked() { + if index > 0 { + to_swap.push(index); // TODO kinda jank but is there a better way? } } - panel_title_ui(ui, panel); - }) - .body(|ui| panel_body_ui(ui, panel, &sources)); + if ui.small_button(" - ").clicked() { + if index < panels_count - 1 { + to_swap.push(index + 1); // TODO kinda jank but is there a better way? + } + } + } + panel_title_ui(ui, panel); + }) + .body(|ui| panel_body_ui(ui, panel, &sources)); } }); }); diff --git a/src/app/worker.rs b/src/app/worker.rs index c06fb7b..0887ea3 100644 --- a/src/app/worker.rs +++ b/src/app/worker.rs @@ -1,10 +1,10 @@ -use std::sync::Arc; -use tracing::warn; +use crate::app::data::{source::fetch, ApplicationState}; use chrono::Utc; use eframe::egui::Context; -use crate::app::data::{ApplicationState, source::fetch}; +use std::sync::Arc; +use tracing::warn; -pub fn native_save(state:Arc) { +pub fn native_save(state: Arc) { std::thread::spawn(move || { let storage = state.storage.lock().expect("Storage Mutex poisoned"); let panels = state.panels.read().expect("Panels RwLock poisoned"); @@ -43,16 +43,16 @@ pub fn native_save(state:Arc) { } pub(crate) trait BackgroundWorker { - fn start(state:Arc, ctx:Context) -> Self; // TODO make it return an error? Can we even do anything without a background worker - fn stop(self); // TODO make it return an error? Can we even do anything without a background worker + fn start(state: Arc, ctx: Context) -> Self; // TODO make it return an error? Can we even do anything without a background worker + fn stop(self); // TODO make it return an error? Can we even do anything without a background worker } pub(crate) struct NativeBackgroundWorker { - worker : std::thread::JoinHandle<()>, + worker: std::thread::JoinHandle<()>, } impl BackgroundWorker for NativeBackgroundWorker { - fn start(state:Arc, ctx:Context) -> Self { + fn start(state: Arc, ctx: Context) -> Self { let worker = std::thread::spawn(move || { let mut last_check = 0; while state.run { @@ -66,25 +66,38 @@ impl BackgroundWorker for NativeBackgroundWorker { for j in 0..sources.len() { let s_id = sources[j].id; if !sources[j].valid() { - let mut last_update = sources[j].last_fetch.write().expect("Sources RwLock poisoned"); + let mut last_update = sources[j] + .last_fetch + .write() + .expect("Sources RwLock poisoned"); *last_update = Utc::now(); let state2 = state.clone(); let url = sources[j].url.clone(); let query_x = sources[j].query_x.clone(); let query_y = sources[j].query_y.clone(); - std::thread::spawn(move || { // TODO this can overspawn if a request takes longer than the refresh interval! + std::thread::spawn(move || { + // TODO this can overspawn if a request takes longer than the refresh interval! match fetch(url.as_str(), query_x.as_str(), query_y.as_str()) { Ok(v) => { - let store = state2.storage.lock().expect("Storage mutex poisoned"); + let store = + state2.storage.lock().expect("Storage mutex poisoned"); if let Err(e) = store.put_value(s_id, v) { warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e); } else { - let sources = state2.sources.read().expect("Sources RwLock poisoned"); - sources[j].data.write().expect("Source data RwLock poisoned").push(v); - let mut last_update = sources[j].last_fetch.write().expect("Source last update RwLock poisoned"); + let sources = + state2.sources.read().expect("Sources RwLock poisoned"); + sources[j] + .data + .write() + .expect("Source data RwLock poisoned") + .push(v); + let mut last_update = sources[j] + .last_fetch + .write() + .expect("Source last update RwLock poisoned"); *last_update = Utc::now(); // overwrite it so fetches comply with API slowdowns and get desynched among them } - }, + } Err(e) => { warn!(target:"background-worker", "Could not fetch value from {} : {:?}", url, e); } @@ -102,12 +115,12 @@ impl BackgroundWorker for NativeBackgroundWorker { } }); - return NativeBackgroundWorker { - worker - }; + return NativeBackgroundWorker { worker }; } fn stop(self) { - self.worker.join().expect("Failed joining main worker thread"); + self.worker + .join() + .expect("Failed joining main worker thread"); } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index b263937..3dc94e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,26 @@ mod app; +use crate::app::{ + data::ApplicationState, + util::InternalLogger, + worker::{BackgroundWorker, NativeBackgroundWorker}, + App, +}; use std::sync::Arc; use tracing_subscriber::prelude::*; -use crate::app::{App, util::InternalLogger, data::ApplicationState, worker::{BackgroundWorker, NativeBackgroundWorker}}; // When compiling natively: #[cfg(not(target_arch = "wasm32"))] fn main() -> ! { - use tracing::metadata::LevelFilter; - + use tracing::metadata::LevelFilter; let native_options = eframe::NativeOptions::default(); - + let mut store_path = dirs::data_dir().unwrap_or(std::path::PathBuf::from(".")); // TODO get cwd more consistently? store_path.push("dashboard.db"); - let store = Arc::new( - ApplicationState::new(store_path).expect("Failed creating application state") - ); + let store = + Arc::new(ApplicationState::new(store_path).expect("Failed creating application state")); tracing_subscriber::registry() .with(LevelFilter::INFO) @@ -25,7 +28,8 @@ fn main() -> ! { .with(InternalLogger::new(store.clone())) .init(); - eframe::run_native( // TODO replace this with a loop that ends so we can cleanly exit the background worker + eframe::run_native( + // TODO replace this with a loop that ends so we can cleanly exit the background worker "dashboard", native_options, Box::new(move |cc| {