diff --git a/Cargo.toml b/Cargo.toml index 30b3577..4e54ebd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dashboard" -version = "0.2.1" +version = "0.3.0" edition = "2021" [[bin]] diff --git a/src/app/data/mod.rs b/src/app/data/mod.rs index 31dba5d..15a354b 100644 --- a/src/app/data/mod.rs +++ b/src/app/data/mod.rs @@ -1,7 +1,7 @@ pub mod source; pub mod store; -use self::source::{Panel, Source}; +use self::source::{Panel, Source, Metric}; use self::store::SQLiteDataStore; use std::num::ParseFloatError; use std::path::PathBuf; @@ -50,6 +50,7 @@ pub struct ApplicationState { pub file_size: RwLock, pub panels: RwLock>, pub sources: RwLock>, + pub metrics: RwLock>, pub storage: Mutex, pub diagnostics: RwLock>, } @@ -60,6 +61,7 @@ impl ApplicationState { let panels = storage.load_panels()?; let sources = storage.load_sources()?; + let metrics = storage.load_metrics()?; return Ok(ApplicationState { run: true, @@ -67,6 +69,7 @@ impl ApplicationState { file_path: path, panels: RwLock::new(panels), sources: RwLock::new(sources), + metrics: RwLock::new(metrics), storage: Mutex::new(storage), diagnostics: RwLock::new(Vec::new()), }); @@ -104,14 +107,10 @@ impl ApplicationState { .lock() .expect("Storage Mutex poisoned") .new_source( - source.panel_id, source.name.as_str(), source.enabled, source.url.as_str(), source.interval, - source.query_x.as_str(), - source.query_y.as_str(), - source.color, self.sources.read().expect("Sources RwLock poisoned").len() as i32, )?; self.sources @@ -120,4 +119,25 @@ impl ApplicationState { .push(verified_source); return Ok(()); } + + pub fn add_metric(&self, metric: &Metric, source: &Source) -> Result<(), FetchError> { + let verified_metric = self + .storage + .lock() + .expect("Storage Mutex poisoned") + .new_metric( + metric.name.as_str(), + source.id, + metric.query_x.as_str(), + metric.query_y.as_str(), + metric.panel_id, + metric.color, + self.metrics.read().expect("Sources RwLock poisoned").len() as i32, // TODO use source.metrics.len() + )?; + self.metrics + .write() + .expect("Sources RwLock poisoned") + .push(verified_metric); + return Ok(()); + } } diff --git a/src/app/data/source.rs b/src/app/data/source.rs index 9fdaf86..4f05421 100644 --- a/src/app/data/source.rs +++ b/src/app/data/source.rs @@ -3,6 +3,7 @@ use chrono::{DateTime, Utc}; use eframe::egui::plot::{Value, Values}; use eframe::epaint::Color32; use std::sync::RwLock; +use tracing::info; #[derive(Debug)] pub struct Panel { @@ -46,14 +47,7 @@ pub struct Source { pub enabled: bool, pub url: String, pub interval: i32, - pub color: Color32, pub(crate) last_fetch: RwLock>, - pub query_x: String, - // pub(crate) compiled_query_x: Arc>, - pub query_y: String, - // pub(crate) compiled_query_y: Arc>, - pub(crate) panel_id: i32, - pub(crate) data: RwLock>, } impl Default for Source { @@ -64,12 +58,7 @@ impl Default for Source { enabled: false, url: "".to_string(), interval: 60, - color: Color32::TRANSPARENT, last_fetch: RwLock::new(Utc::now()), - query_x: "".to_string(), - query_y: "".to_string(), - panel_id: -1, - data: RwLock::new(Vec::new()), } } } @@ -81,7 +70,10 @@ fn avg_value(values: &[Value]) -> Value { x += v.x; y += v.y; } - return Value { x: x / values.len() as f64, y: y / values.len() as f64 }; + return Value { + x: x / values.len() as f64, + y: y / values.len() as f64, + }; } impl Source { @@ -90,8 +82,64 @@ impl Source { return (Utc::now() - *last_fetch).num_seconds() < self.interval as i64; } - // TODO optimize this with caching! - pub fn values(&self, min_x: Option, max_x: Option, chunk_size: Option) -> Values { + pub fn fetch(&self) -> Result { + fetch(self.url.as_str()) + } +} + +pub fn fetch(url: &str) -> Result { + return Ok(ureq::get(url).call()?.into_json()?); +} + +#[derive(Debug)] +pub struct Metric { + pub(crate) id: i32, + pub name: String, + pub source_id: i32, + pub color: Color32, + pub query_x: String, + pub query_y: String, + pub(crate) panel_id: i32, + pub(crate) data: RwLock>, +} + +impl Default for Metric { + fn default() -> Self { + Metric { + id: -1, + name: "".to_string(), + source_id: -1, + color: Color32::TRANSPARENT, + query_x: "".to_string(), + query_y: "".to_string(), + panel_id: -1, + data: RwLock::new(Vec::new()), + } + } +} + +impl Metric { + pub fn extract(&self, value: &serde_json::Value) -> Result { + let x: f64; + if self.query_x.len() > 0 { + x = jql::walker(value, self.query_x.as_str())? + .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(value, self.query_y.as_str())? + .as_f64() + .ok_or(FetchError::JQLError("Y query is null".to_string()))?; + Ok(Value { x, y }) + } + + pub fn values( + &self, + min_x: Option, + max_x: Option, + chunk_size: Option, + ) -> Values { let mut values = self.data.read().expect("Values RwLock poisoned").clone(); if let Some(min_x) = min_x { values.retain(|x| x.x > min_x); @@ -100,27 +148,12 @@ impl Source { values.retain(|x| x.x < max_x); } if let Some(chunk_size) = chunk_size { - if chunk_size > 0 { // TODO make this nested if prettier + if chunk_size > 0 { + // TODO make this nested if prettier let iter = values.chunks(chunk_size as usize); - values = iter.map(|x| avg_value(x) ).collect(); + values = iter.map(|x| avg_value(x)).collect(); } } Values::from_values(values) } } - -pub fn fetch(url: &str, query_x: &str, query_y: &str) -> Result { - let res = ureq::get(url).call()?.into_json()?; - 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? - } 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 }); -} diff --git a/src/app/data/store.rs b/src/app/data/store.rs index 8bc171a..0a4d6aa 100644 --- a/src/app/data/store.rs +++ b/src/app/data/store.rs @@ -8,6 +8,8 @@ use eframe::egui::{plot::Value, Color32}; use rusqlite::{params, Connection}; use std::sync::RwLock; +use super::source::Metric; + pub trait DataStorage { fn add_panel(&self, name: &str); } @@ -46,19 +48,38 @@ impl SQLiteDataStore { enabled BOOL NOT NULL, url TEXT NOT NULL, interval INT NOT NULL, - query_x TEXT NOT NULL, - query_y TEXT NOT NULL, - panel_id INT NOT NULL, - color INT NULL, position INT NOT NULL );", [], )?; + conn.execute( + "CREATE TABLE IF NOT EXISTS metrics ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + source_id INT NOT NULL, + query_x TEXT NOT NULL, + query_y TEXT NOT NULL, + panel_id INT NOT NULL, + color INT NOT NULL, + position INT NOT NULL + );", + [], + )?; + +// BEGIN TRANSACTION; +// CREATE TEMPORARY TABLE t1_backup(a,b); +// INSERT INTO t1_backup SELECT a,b FROM t1; +// DROP TABLE t1; +// CREATE TABLE t1(a,b); +// INSERT INTO t1 SELECT a,b FROM t1_backup; +// DROP TABLE t1_backup; +// COMMIT; + conn.execute( "CREATE TABLE IF NOT EXISTS points ( id INTEGER PRIMARY KEY, - source_id INT NOT NULL, + metric_id INT NOT NULL, x FLOAT NOT NULL, y FLOAT NOT NULL );", @@ -68,12 +89,12 @@ impl SQLiteDataStore { Ok(SQLiteDataStore { conn }) } - pub fn load_values(&self, source_id: i32) -> rusqlite::Result> { + pub fn load_values(&self, metric_id: i32) -> rusqlite::Result> { let mut values: Vec = Vec::new(); let mut statement = self .conn - .prepare("SELECT x, y FROM points WHERE source_id = ?")?; - let values_iter = statement.query_map(params![source_id], |row| { + .prepare("SELECT x, y FROM points WHERE metric_id = ?")?; + let values_iter = statement.query_map(params![metric_id], |row| { Ok(Value { x: row.get(0)?, y: row.get(1)?, @@ -89,10 +110,10 @@ impl SQLiteDataStore { Ok(values) } - pub fn put_value(&self, source_id: i32, v: Value) -> rusqlite::Result { + pub fn put_value(&self, metric_id: i32, v: Value) -> rusqlite::Result { self.conn.execute( - "INSERT INTO points(source_id, x, y) VALUES (?, ?, ?)", - params![source_id, v.x, v.y], + "INSERT INTO points(metric_id, x, y) VALUES (?, ?, ?)", + params![metric_id, v.x, v.y], ) } @@ -107,17 +128,11 @@ impl SQLiteDataStore { url: row.get(3)?, interval: row.get(4)?, last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), - query_x: row.get(5)?, - query_y: row.get(6)?, - panel_id: row.get(7)?, - color: unpack_color(row.get(8).unwrap_or(0)), - data: RwLock::new(Vec::new()), }) })?; for source in sources_iter { - if let Ok(mut s) = source { - s.data = RwLock::new(self.load_values(s.id)?); + if let Ok(s) = source { sources.push(s); } } @@ -128,41 +143,28 @@ impl SQLiteDataStore { // jank! TODO make it not jank! pub fn new_source( &self, - panel_id: i32, name: &str, enabled: bool, url: &str, interval: i32, - query_x: &str, - query_y: &str, - color: Color32, position: i32, ) -> rusqlite::Result { - let color_u32: Option = if color == Color32::TRANSPARENT { - None - } else { - Some(repack_color(color)) - }; self.conn.execute( - "INSERT INTO sources(name, enabled, url, interval, query_x, query_y, panel_id, color, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - params![name, enabled, url, interval, query_x, query_y, panel_id, color_u32, position], + "INSERT INTO sources(name, enabled, url, interval, position) VALUES (?, ?, ?, ?, ?)", + params![name, enabled, url, interval, position], )?; let mut statement = self .conn - .prepare("SELECT * FROM sources WHERE name = ? AND panel_id = ?")?; - for panel in statement.query_map(params![name, panel_id], |row| { + .prepare("SELECT * FROM sources WHERE name = ? AND url = ? ORDER BY id DESC")?; + for panel in statement.query_map(params![name, url], |row| { Ok(Source { id: row.get(0)?, name: row.get(1)?, enabled: row.get(2)?, url: row.get(3)?, interval: row.get(4)?, + // position: row.get(5)?, last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), - query_x: row.get(5)?, - query_y: row.get(6)?, - panel_id: row.get(7)?, - color: unpack_color(row.get(8).unwrap_or(0)), - data: RwLock::new(Vec::new()), }) })? { if let Ok(p) = panel { @@ -175,25 +177,16 @@ impl SQLiteDataStore { pub fn update_source( &self, - source_id: i32, - panel_id: i32, + id: i32, name: &str, enabled: bool, url: &str, interval: i32, - query_x: &str, - query_y: &str, - color: Color32, position: i32, ) -> rusqlite::Result { - let color_u32: Option = if color == Color32::TRANSPARENT { - None - } else { - Some(repack_color(color)) - }; self.conn.execute( - "UPDATE sources SET name = ?, enabled = ?, url = ?, interval = ?, query_x = ?, query_y = ?, panel_id = ?, color = ?, position = ? WHERE id = ?", - params![name, enabled, url, interval, query_x, query_y, panel_id, color_u32, position, source_id], + "UPDATE sources SET name = ?, enabled = ?, url = ?, interval = ?, position = ? WHERE id = ?", + params![name, enabled, url, interval, position, id], ) } @@ -201,6 +194,93 @@ impl SQLiteDataStore { // self.conn.execute("DELETE FROM sources WHERE id = ?", params![id]) // } + pub fn load_metrics(&self) -> rusqlite::Result> { + let mut metrics: Vec = Vec::new(); + let mut statement = self.conn.prepare("SELECT * FROM metrics ORDER BY position")?; + let metrics_iter = statement.query_map([], |row| { + Ok(Metric { + id: row.get(0)?, + name: row.get(1)?, + source_id: row.get(2)?, + query_x: row.get(3)?, + query_y: row.get(4)?, + panel_id: row.get(5)?, + color: unpack_color(row.get(6).unwrap_or(0)), + // position: row.get(7)?, + data: RwLock::new(Vec::new()), + }) + })?; + + for metric in metrics_iter { + if let Ok(m) = metric { + *m.data.write().expect("Points RwLock poisoned") = self.load_values(m.id)?; + metrics.push(m); + } + } + + Ok(metrics) + } + + // jank! TODO make it not jank! + pub fn new_metric( + &self, + name: &str, + source_id: i32, + query_x: &str, + query_y: &str, + panel_id: i32, + color: Color32, + position: i32, + ) -> rusqlite::Result { + self.conn.execute( + "INSERT INTO metrics(name, source_id, query_x, query_y, panel_id, color, position) VALUES (?, ?, ?, ?, ?, ?, ?)", + params![name, source_id, query_x, query_y, panel_id, repack_color(color), position], + )?; + let mut statement = self + .conn + .prepare("SELECT * FROM metrics WHERE source_id = ? AND panel_id = ? AND name = ? ORDER BY id DESC")?; + for metric in statement.query_map(params![source_id, panel_id, name], |row| { + Ok(Metric { + id: row.get(0)?, + name: row.get(1)?, + source_id: row.get(2)?, + query_x: row.get(3)?, + query_y: row.get(4)?, + panel_id: row.get(5)?, + color: unpack_color(row.get(6).unwrap_or(0)), + // position: row.get(7)?, + data: RwLock::new(Vec::new()), + }) + })? { + if let Ok(m) = metric { + return Ok(m); + } + } + + Err(rusqlite::Error::QueryReturnedNoRows) + } + + pub fn update_metric( + &self, + id: i32, + name: &str, + source_id: i32, + query_x: &str, + query_y: &str, + panel_id: i32, + color: Color32, + position: i32, + ) -> rusqlite::Result { + self.conn.execute( + "UPDATE metrics SET name = ?, query_x = ?, query_y = ?, panel_id = ?, color = ?, position = ? WHERE id = ? AND source_id = ?", + params![name, query_x, query_y, panel_id, repack_color(color), position, id, source_id], + ) + } + + // pub fn delete_metric(&self, id:i32) -> rusqlite::Result { + // self.conn.execute("DELETE FROM metrics WHERE id = ?", params![id]) + // } + pub fn load_panels(&self) -> rusqlite::Result> { let mut panels: Vec = Vec::new(); let mut statement = self diff --git a/src/app/gui/metric.rs b/src/app/gui/metric.rs new file mode 100644 index 0000000..70745b0 --- /dev/null +++ b/src/app/gui/metric.rs @@ -0,0 +1,67 @@ +use eframe::{egui::{Ui, TextEdit, ComboBox, Layout, Sense, color_picker::show_color_at}, emath::Align, epaint::Color32}; + +use crate::app::data::source::{Panel, Metric}; + +fn color_square(ui: &mut Ui, color:Color32) { + let size = ui.spacing().interact_size; + let (rect, response) = ui.allocate_exact_size(size, Sense::click()); + if ui.is_rect_visible(rect) { + let visuals = ui.style().interact(&response); + let rect = rect.expand(visuals.expansion); + + show_color_at(ui.painter(), color, rect); + + let rounding = visuals.rounding.at_most(2.0); + ui.painter() + .rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border + } +} + +pub fn metric_display_ui(ui: &mut Ui, metric: &Metric, _width: f32) { + ui.horizontal(|ui| { + color_square(ui, metric.color); + ui.label(&metric.name); + ui.with_layout(Layout::top_down(Align::RIGHT), |ui| { + ui.horizontal(|ui| { + ui.label(format!("panel: {}", metric.panel_id)); + if metric.query_y.len() > 0 { + ui.label(format!("y: {}", metric.query_y)); + } + if metric.query_x.len() > 0 { + ui.label(format!("x: {}", metric.query_x)); + } + }) + }); + }); +} + +pub fn metric_edit_ui(ui: &mut Ui, metric: &mut Metric, panels: Option<&Vec>, width: f32) { + let text_width = width - 190.0; + ui.horizontal(|ui| { + ui.color_edit_button_srgba(&mut metric.color); + TextEdit::singleline(&mut metric.name) + .desired_width(text_width / 2.0) + .hint_text("name") + .show(ui); + ui.separator(); + TextEdit::singleline(&mut metric.query_x) + .desired_width(text_width / 4.0) + .hint_text("x") + .show(ui); + TextEdit::singleline(&mut metric.query_y) + .desired_width(text_width / 4.0) + .hint_text("y") + .show(ui); + if let Some(panels) = panels { + ComboBox::from_id_source(format!("panel-selector-{}", metric.id)) + .width(60.0) + .selected_text(format!("panel: {}", metric.panel_id)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut metric.panel_id, -1, "None"); + for p in panels { + ui.selectable_value(&mut metric.panel_id, p.id, p.name.as_str()); + } + }); + } + }); +} diff --git a/src/app/gui/mod.rs b/src/app/gui/mod.rs index cf2695c..8d41f82 100644 --- a/src/app/gui/mod.rs +++ b/src/app/gui/mod.rs @@ -1,2 +1,3 @@ pub mod panel; pub mod source; +pub mod metric; diff --git a/src/app/gui/panel.rs b/src/app/gui/panel.rs index 59e1df9..f792cf7 100644 --- a/src/app/gui/panel.rs +++ b/src/app/gui/panel.rs @@ -5,7 +5,7 @@ use eframe::{egui::{ }, emath::Vec2}; use crate::app::{ - data::source::{Panel, Source}, + data::source::{Panel, Metric}, util::timestamp_to_str, }; @@ -24,30 +24,30 @@ pub fn panel_title_ui(ui: &mut Ui, panel: &mut Panel, extra: bool) { ui.toggle_value(&mut panel.view_scroll, "🔒"); ui.separator(); if panel.limit { - ui.label("min"); // TODO makes no sense if it's not a timeserie ui.add( DragValue::new(&mut panel.view_size) .speed(10) + .suffix(" min") .clamp_range(0..=2147483647i32), ); } ui.toggle_value(&mut panel.limit, "limit"); ui.separator(); if panel.shift { - ui.label("min"); ui.add( DragValue::new(&mut panel.view_offset) .speed(10) + .suffix(" min") .clamp_range(0..=2147483647i32), ); } ui.toggle_value(&mut panel.shift, "offset"); ui.separator(); if panel.reduce { - ui.label("x"); ui.add( DragValue::new(&mut panel.view_chunks) .speed(1) + .prefix("x") .clamp_range(1..=1000), // TODO allow to average larger spans maybe? ); } @@ -63,7 +63,7 @@ pub fn panel_title_ui(ui: &mut Ui, panel: &mut Panel, extra: bool) { }); } -pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec) { +pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, metrics: &Vec) { let mut p = Plot::new(format!("plot-{}", panel.name)) .height(panel.height as f32) .allow_scroll(false) @@ -135,11 +135,13 @@ pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec) { let min_x = if panel.limit { Some(_now - _size - _off) } else { None }; let max_x = if panel.shift { Some(_now - _off) } else { None }; let chunk_size = if panel.reduce { Some(panel.view_chunks) } else { None }; - for source in sources { - if source.panel_id == panel.id { + for metric in metrics { + if metric.panel_id == panel.id { // let chunks = None; - let line = Line::new(source.values(min_x, max_x, chunk_size)).name(source.name.as_str()); - plot_ui.line(line.color(source.color)); + let line = Line::new(metric.values(min_x, max_x, chunk_size)) + .name(metric.name.as_str()) + .color(metric.color); + plot_ui.line(line); } } }); diff --git a/src/app/gui/source.rs b/src/app/gui/source.rs index 2a1ae8b..29c78e9 100644 --- a/src/app/gui/source.rs +++ b/src/app/gui/source.rs @@ -1,81 +1,41 @@ -use eframe::egui::{Ui, TextEdit, ComboBox, Slider, DragValue}; +use eframe::{egui::{Ui, TextEdit, DragValue, Checkbox}}; -use crate::app::data::source::{Panel, Source}; +use crate::app::data::source::{Panel, Source, Metric}; -#[allow(dead_code)] -pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec) { - TextEdit::singleline(&mut source.name) - .hint_text("name") - .desired_width(50.0) - .show(ui); - TextEdit::singleline(&mut source.url) - .hint_text("url") - .desired_width(160.0) - .show(ui); - TextEdit::singleline(&mut source.query_x) - .hint_text("x") - .desired_width(30.0) - .show(ui); - TextEdit::singleline(&mut source.query_y) - .hint_text("y") - .desired_width(30.0) - .show(ui); - ComboBox::from_id_source("panel") - .selected_text(format!("panel [{}]", source.panel_id)) - .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.checkbox(&mut source.enabled, "enabled"); - ui.add(Slider::new(&mut source.interval, 1..=60)); - ui.color_edit_button_srgba(&mut source.color); -} +use super::metric::{metric_edit_ui, metric_display_ui}; -pub fn source_edit_ui(ui: &mut Ui, source: &mut Source, panels: &Vec, width: f32) { - ui.group(|ui| { - ui.vertical(|ui| { - ui.horizontal(|ui| { - let text_width = width - 25.0; - ui.checkbox(&mut source.enabled, ""); - TextEdit::singleline(&mut source.name) - .desired_width(text_width / 4.0) - .hint_text("name") - .show(ui); - TextEdit::singleline(&mut source.url) - .desired_width(text_width * 3.0 / 4.0) - .hint_text("url") - .show(ui); - }); - ui.horizontal(|ui| { - let text_width : f32 ; - if width > 400.0 { - ui.add(Slider::new(&mut source.interval, 1..=120)); - text_width = width - 330.0 - } else { - ui.add(DragValue::new(&mut source.interval).clamp_range(1..=120)); - text_width = width - 225.0 - } - TextEdit::singleline(&mut source.query_x) - .desired_width(text_width / 2.0) - .hint_text("x") - .show(ui); - TextEdit::singleline(&mut source.query_y) - .desired_width(text_width / 2.0) - .hint_text("y") - .show(ui); - ComboBox::from_id_source(format!("panel-{}", source.id)) - .width(60.0) - .selected_text(format!("panel: {}", source.panel_id)) - .show_ui(ui, |ui| { - ui.selectable_value(&mut source.panel_id, -1, "None"); - for p in panels { - ui.selectable_value(&mut source.panel_id, p.id, p.name.as_str()); - } - }); - ui.color_edit_button_srgba(&mut source.color); - }); - }); +pub fn source_display_ui(ui: &mut Ui, source: &mut Source, metrics: &Vec, _width: f32) { + ui.horizontal(|ui| { + ui.add_enabled(false, Checkbox::new(&mut source.enabled, "")); + ui.add_enabled(false, DragValue::new(&mut source.interval).clamp_range(1..=120)); + ui.heading(&source.name).on_hover_text(&source.url); }); + for metric in metrics.iter() { + if metric.source_id == source.id { + metric_display_ui(ui, metric, ui.available_width()); + } + } +} + +pub fn source_edit_ui(ui: &mut Ui, source: &mut Source, metrics: Option<&mut Vec>, panels: &Vec, width: f32) { + ui.horizontal(|ui| { + let text_width = width - 100.0; + ui.checkbox(&mut source.enabled, ""); + ui.add(DragValue::new(&mut source.interval).clamp_range(1..=3600)); + TextEdit::singleline(&mut source.name) + .desired_width(text_width / 4.0) + .hint_text("name") + .show(ui); + TextEdit::singleline(&mut source.url) + .desired_width(text_width * 3.0 / 4.0) + .hint_text("url") + .show(ui); + }); + if let Some(metrics) = metrics { + for metric in metrics.iter_mut() { + if metric.source_id == source.id { + metric_edit_ui(ui, metric, Some(panels), width - 10.0); + } + } + } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 5ceec67..345d489 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,18 +11,21 @@ use eframe::emath::Align; use std::sync::Arc; use tracing::error; -use self::data::source::{Panel, Source}; +use self::data::source::{Metric, Panel, Source}; use self::data::ApplicationState; +use self::gui::metric::metric_edit_ui; use self::gui::panel::{panel_body_ui, panel_edit_inline_ui, panel_title_ui}; -use self::gui::source::source_edit_ui; +use self::gui::source::{source_display_ui, source_edit_ui}; use self::util::human_size; use self::worker::native_save; pub struct App { data: Arc, + input_metric: Metric, input_source: Source, input_panel: Panel, edit: bool, + sources: bool, padding: bool, } @@ -30,9 +33,11 @@ impl App { pub fn new(_cc: &eframe::CreationContext, data: Arc) -> Self { Self { data, + input_metric: Metric::default(), input_panel: Panel::default(), input_source: Source::default(), edit: false, + sources: true, padding: false, } } @@ -45,6 +50,8 @@ impl eframe::App for App { global_dark_light_mode_switch(ui); ui.heading("dashboard"); ui.separator(); + ui.checkbox(&mut self.sources, "sources"); + ui.separator(); ui.checkbox(&mut self.edit, "edit"); if self.edit { if ui.button("save").clicked() { @@ -118,48 +125,98 @@ impl eframe::App for App { }); }); }); - if self.edit { + if self.sources { let mut to_swap: Option = None; // let mut to_delete: Option = None; SidePanel::left("sources-bar") .width_range(280.0..=800.0) - .default_width(500.0) + .default_width(350.0) .show(ctx, |ui| { let panels = self.data.panels.read().expect("Panels RwLock poisoned"); - ScrollArea::vertical().show(ui, |ui| { - let panel_width = ui.available_width(); + let panel_width = ui.available_width(); + ScrollArea::both().max_width(panel_width).show(ui, |ui| { + // TODO only vertical! { - let mut sources = self.data.sources.write().expect("Sources RwLock poisoned"); + let mut sources = + self.data.sources.write().expect("Sources RwLock poisoned"); let sources_count = sources.len(); + ui.heading("Sources"); + ui.separator(); for (index, source) in sources.iter_mut().enumerate() { ui.horizontal(|ui| { + if self.edit { + ui.vertical(|ui| { + ui.add_space(10.0); + if ui.small_button("+").clicked() { + if index > 0 { + to_swap = Some(index); // TODO kinda jank but is there a better way? + } + } + if ui.small_button("−").clicked() { + if index < sources_count - 1 { + to_swap = Some(index + 1); // TODO kinda jank but is there a better way? + } + } + }); + } ui.vertical(|ui| { - ui.add_space(10.0); - if ui.small_button("+").clicked() { - if index > 0 { - to_swap = Some(index); // TODO kinda jank but is there a better way? - } - } - if ui.small_button("−").clicked() { - if index < sources_count - 1 { - to_swap = Some(index + 1); // TODO kinda jank but is there a better way? - } + let remaining_width = ui.available_width(); + if self.edit { + ui.group(|ui| { + source_edit_ui( + ui, + source, + Some(&mut *self.data.metrics.write().expect("Metrics RwLock poisoned")), + &panels, + remaining_width, + ); + ui.horizontal(|ui| { + metric_edit_ui( + ui, + &mut self.input_metric, + None, + remaining_width - 10.0, + ); + ui.add_space(5.0); + if ui.button(" × ").clicked() { + self.input_metric = Metric::default(); + } + ui.separator(); + if ui.button(" + ").clicked() { + if let Err(e) = self + .data + .add_metric(&self.input_metric, source) + { + error!(target: "ui", "Error adding metric : {:?}", e); + } + } + }) + }); + } else { + let metrics = + self.data.metrics.read().expect("Metrics RwLock poisoned"); + source_display_ui( + ui, + source, + &metrics, + remaining_width, + ); + ui.separator(); } }); - let remaining_width = ui.available_width(); - source_edit_ui(ui, source, &panels, remaining_width); }); } } - ui.add_space(20.0); - ui.separator(); - ui.horizontal(|ui| { - ui.heading("new source"); + if self.edit { + ui.add_space(20.0); + ui.separator(); + ui.horizontal(|ui| { + ui.heading("new source"); ui.with_layout(Layout::top_down(Align::RIGHT), |ui| { ui.horizontal(|ui| { if ui.button("add").clicked() { if let Err(e) = self.data.add_source(&self.input_source) { - error!(target: "ui", "Error adding souce : {:?}", e); + error!(target: "ui", "Error adding source : {:?}", e); } else { self.input_source.id += 1; } @@ -167,10 +224,17 @@ impl eframe::App for App { ui.toggle_value(&mut self.padding, "#"); }); }); - }); - source_edit_ui(ui, &mut self.input_source, &panels, panel_width); - if self.padding { - ui.add_space(300.0); + }); + source_edit_ui( + ui, + &mut self.input_source, + None, + &panels, + panel_width, + ); + if self.padding { + ui.add_space(300.0); + } } }); }); @@ -191,7 +255,7 @@ impl eframe::App for App { ScrollArea::vertical().show(ui, |ui| { let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); // TODO only lock as write when editing let panels_count = panels.len(); - let sources = self.data.sources.read().expect("Sources RwLock poisoned"); // TODO only lock as write when editing + let metrics = self.data.metrics.read().expect("Metrics RwLock poisoned"); // TODO only lock as write when editing for (index, panel) in panels.iter_mut().enumerate() { if index > 0 { ui.separator(); @@ -220,19 +284,31 @@ impl eframe::App for App { } panel_title_ui(ui, panel, self.edit); }) - .body(|ui| panel_body_ui(ui, panel, &sources)); + .body(|ui| panel_body_ui(ui, panel, &metrics)); } }); }); if let Some(i) = to_delete { // TODO can this be done in background? idk let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); - if let Err(e) = self.data.storage.lock().expect("Storage Mutex poisoned").delete_panel(panels[i].id) { + if let Err(e) = self + .data + .storage + .lock() + .expect("Storage Mutex poisoned") + .delete_panel(panels[i].id) + { error!(target: "ui", "Could not delete panel : {:?}", e); } else { - for source in self.data.sources.write().expect("Sources RwLock poisoned").iter_mut() { - if source.panel_id == panels[i].id { - source.panel_id = -1; + for metric in self + .data + .metrics + .write() + .expect("Sources RwLock poisoned") + .iter_mut() + { + if metric.panel_id == panels[i].id { + metric.panel_id = -1; } } panels.remove(i); diff --git a/src/app/util.rs b/src/app/util.rs index cc3922a..5dcdfd2 100644 --- a/src/app/util.rs +++ b/src/app/util.rs @@ -46,6 +46,7 @@ pub fn unpack_color(c: u32) -> Color32 { return Color32::from_rgba_unmultiplied(r, g, b, a); } +#[allow(dead_code)] pub fn repack_color(c: Color32) -> u32 { let mut out: u32 = 0; let mut offset = 0; diff --git a/src/app/worker.rs b/src/app/worker.rs index 753c5b7..d3d00a8 100644 --- a/src/app/worker.rs +++ b/src/app/worker.rs @@ -26,22 +26,33 @@ pub fn native_save(state: Arc) { ) { warn!(target: "native-save", "Could not update panel #{} : {:?}", panel.id, e); } - let sources = state.sources.read().expect("Sources RwLock poisoned"); - for (index, source) in sources.iter().enumerate() { - if let Err(e) = storage.update_source( - source.id, - source.panel_id, - source.name.as_str(), - source.enabled, - source.url.as_str(), - source.interval, - source.query_x.as_str(), - source.query_y.as_str(), - source.color, - index as i32, - ) { - warn!(target: "native-save", "Could not update source #{} : {:?}", source.id, e); - } + } + let sources = state.sources.read().expect("Sources RwLock poisoned"); + for (index, source) in sources.iter().enumerate() { + if let Err(e) = storage.update_source( + source.id, + source.name.as_str(), + source.enabled, + source.url.as_str(), + source.interval, + index as i32, + ) { + warn!(target: "native-save", "Could not update source #{} : {:?}", source.id, e); + } + } + let metrics = state.metrics.read().expect("Metrics RwLock poisoned"); + for (index, metric) in metrics.iter().enumerate() { + if let Err(e) = storage.update_metric( + metric.id, + metric.name.as_str(), + metric.source_id, + metric.query_x.as_str(), + metric.query_y.as_str(), + metric.panel_id, + metric.color, + index as i32, + ) { + warn!(target: "native-save", "Could not update metric #{} : {:?}", metric.id, e); } } }); @@ -78,30 +89,33 @@ impl BackgroundWorker for NativeBackgroundWorker { *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! - match fetch(url.as_str(), query_x.as_str(), query_y.as_str()) { - Ok(v) => { + match fetch(url.as_str()) { + Ok(res) => { 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"); - *last_update = Utc::now(); // overwrite it so fetches comply with API slowdowns and get desynched among them + for metric in state2.metrics.read().expect("Metrics RwLock poisoned").iter() { + if metric.source_id == s_id { + match metric.extract(&res) { + Ok(v) => { + metric.data.write().expect("Data RwLock poisoned").push(v); + if let Err(e) = store.put_value(metric.id, v) { + warn!(target:"background-worker", "Could not put sample for source #{} in db: {:?}", s_id, e); + } + } + Err(e) => { + warn!(target:"background-worker", "[{}] Could not extract value from result: {:?}", metric.name, e); // TODO: more info! + } + } + } } + let sources = state2.sources.read().expect("Sources RwLock poisoned"); + 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);