diff --git a/src/data/entities/metrics.rs b/src/data/entities/metrics.rs index a9999b0..16d9ccb 100644 --- a/src/data/entities/metrics.rs +++ b/src/data/entities/metrics.rs @@ -13,7 +13,7 @@ pub struct Model { pub id: i64, pub name: String, pub source_id: i64, - pub query_x: Option, + pub query_x: String, pub query_y: String, pub panel_id: i64, pub color: i32, @@ -29,14 +29,8 @@ impl ActiveModelBehavior for ActiveModel {} impl Model { pub fn extract(&self, value: &serde_json::Value) -> Result { let x: f64; - let fallback_query = "".into(); - let q_x = self.query_x.as_ref().unwrap_or(&fallback_query); - // TODO because of bad design, empty queries are - // empty strings in my db. Rather than converting - // them right away, I'm putting this jank fix: - // checking len - if q_x.len() > 0 { - x = jql::walker(value, q_x.as_str())? + 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 { @@ -55,7 +49,7 @@ impl Default for Model { id: 0, name: "".into(), source_id: 0, - query_x: None, + query_x: "".into(), query_y: "".into(), panel_id: 0, color: 0, diff --git a/src/gui/metric.rs b/src/gui/metric.rs index 4778a50..92054ec 100644 --- a/src/gui/metric.rs +++ b/src/gui/metric.rs @@ -36,8 +36,7 @@ pub fn _metric_display_ui(ui: &mut Ui, metric: &entities::metrics::Model, _width pub fn metric_edit_ui(ui: &mut Ui, metric: &entities::metrics::Model, panels: Option<&Vec>, width: f32) { let text_width = width - 195.0; let mut name = metric.name.clone(); - let def_str = "".into(); - let mut query_x = metric.query_x.as_ref().unwrap_or(&def_str).clone(); + let mut query_x = metric.query_x.clone(); let mut query_y = metric.query_y.clone(); let mut panel_id = 0; ui.horizontal(|ui| { diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 9bb41b0..472db7c 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -9,7 +9,7 @@ use eframe::egui::{CentralPanel, Context, SidePanel, TopBottomPanel, Window}; use tokio::sync::watch; use tracing::error; -use crate::{data::entities, worker::visualizer::AppStateView}; +use crate::{data::entities, worker::{visualizer::AppStateView, BackgroundAction}}; use panel::main_content; use scaffold::{ // confirmation_popup_delete_metric, confirmation_popup_delete_source, footer, @@ -17,7 +17,7 @@ use scaffold::{ }; use source::source_panel; -use self::scaffold::footer; +use self::scaffold::{footer, popup_edit_ui, EditingModel}; pub struct App { view: AppStateView, @@ -29,11 +29,12 @@ pub struct App { width_tx: watch::Sender, logger_view: watch::Receiver>, - buffer_panel: entities::panels::Model, + // buffer_panel: entities::panels::Model, buffer_source: entities::sources::Model, - buffer_metric: entities::metrics::Model, + // buffer_metric: entities::metrics::Model, edit: bool, + editing: Vec, sidebar: bool, padding: bool, // windows: Vec>, @@ -51,11 +52,10 @@ impl App { let panels = view.panels.borrow().clone(); Self { db_path, interval, panels, width_tx, view, logger_view, - buffer_panel: entities::panels::Model::default(), buffer_source: entities::sources::Model::default(), - buffer_metric: entities::metrics::Model::default(), last_redraw: 0, edit: false, + editing: vec![], sidebar: true, padding: false, // windows: vec![], @@ -75,6 +75,12 @@ impl App { error!(target: "app", "Could not request flush: {:?}", e); } } + + pub fn op(&self, op: BackgroundAction) { + if let Err(e) = self.view.op.blocking_send(op) { + error!(target: "app", "Could not send operation: {:?}", e); + } + } } impl eframe::App for App { @@ -87,20 +93,9 @@ impl eframe::App for App { footer(ctx, ui, self.logger_view.clone(), self.db_path.clone(), self.view.points.borrow().len()); }); - let _w = Window::new("a"); - - // if let Some(index) = self.deleting_metric { - // Window::new(format!("Delete Metric #{}?", index)) - // .show(ctx, |ui| confirmation_popup_delete_metric(self, ui, index)); - // } - // if let Some(index) = self.deleting_source { - // Window::new(format!("Delete Source #{}?", index)) - // .show(ctx, |ui| confirmation_popup_delete_source(self, ui, index)); - // } - - // for window in self.windows { - - // } + for m in self.editing.iter_mut() { + Window::new(m.id_repr()).show(ctx, |ui| popup_edit_ui(ui, m)); + } if self.sidebar { SidePanel::left("sources-bar") @@ -113,7 +108,7 @@ impl eframe::App for App { main_content(self, ctx, ui); }); - if let Some(viewsize) = self.panels.iter().map(|p| p.view_size).max() { + if let Some(viewsize) = self.panels.iter().map(|p| p.view_size + p.view_offset).max() { if let Err(e) = self.width_tx.send(viewsize as i64) { error!(target: "app", "Could not update fetch size : {:?}", e); } @@ -123,5 +118,13 @@ impl eframe::App for App { ctx.request_repaint(); self.last_redraw = Utc::now().timestamp(); } + + for m in self.editing.iter() { + if m.should_fetch() { + self.op(m.to_msg()); + } + } + + self.editing.retain(|v| v.modifying()); } } diff --git a/src/gui/panel.rs b/src/gui/panel.rs index 9d151c6..9037728 100644 --- a/src/gui/panel.rs +++ b/src/gui/panel.rs @@ -46,7 +46,7 @@ pub fn main_content(app: &mut App, ctx: &Context, ui: &mut Ui) { }); } -pub fn panel_edit_inline_ui(_ui: &mut Ui, _panel: &entities::panels::Model) { +pub fn _panel_edit_inline_ui(_ui: &mut Ui, _panel: &entities::panels::Model) { // TextEdit::singleline(&mut panel.name) // .hint_text("name") // .desired_width(100.0) diff --git a/src/gui/scaffold.rs b/src/gui/scaffold.rs index e995ffd..336c618 100644 --- a/src/gui/scaffold.rs +++ b/src/gui/scaffold.rs @@ -1,9 +1,8 @@ -use eframe::{Frame, egui::{collapsing_header::CollapsingState, Context, Ui, Layout, ScrollArea, global_dark_light_mode_switch}, emath::Align}; +use eframe::{Frame, egui::{collapsing_header::CollapsingState, Context, Ui, Layout, ScrollArea, global_dark_light_mode_switch, TextEdit, Checkbox, Slider}, emath::Align}; +use sea_orm::{Set, Unchanged, ActiveValue::NotSet}; use tokio::sync::watch; -use crate::gui::App; - -use super::panel::panel_edit_inline_ui; +use crate::{gui::App, data::entities, util::unpack_color, worker::BackgroundAction}; // TODO make this not super specific! pub fn _confirmation_popup_delete_metric(_app: &mut App, ui: &mut Ui, _metric_index: usize) { @@ -58,6 +57,166 @@ pub fn _confirmation_popup_delete_source(_app: &mut App, ui: &mut Ui, _source_in }); } +pub struct EditingModel { + pub id: i64, + m: EditingModelType, + new: bool, + valid: bool, + ready: bool, +} + +impl EditingModel { + pub fn id_repr(&self) -> String { + let prefix = match self.m { + EditingModelType::EditingPanel { panel: _ } => "panel", + EditingModelType::EditingSource { source: _ } => "source", + EditingModelType::EditingMetric { metric: _ } => "metric", + }; + format!("edit_{}_{}", prefix, self.id) + } + + pub fn should_fetch(&self) -> bool { + return self.ready && self.valid; + } + + pub fn modifying(&self) -> bool { + return !self.ready; + } + + pub fn to_msg(&self) -> BackgroundAction { + match &self.m { + EditingModelType::EditingPanel { panel } => + BackgroundAction::UpdatePanel { + panel: entities::panels::ActiveModel { + id: if self.new { NotSet } else { Unchanged(panel.id) }, + name: Set(panel.name.clone()), + view_scroll: Set(panel.view_scroll), + view_size: Set(panel.view_size), + timeserie: Set(panel.timeserie), + height: Set(panel.height), + limit_view: Set(panel.limit_view), + position: Set(panel.position), + reduce_view: Set(panel.reduce_view), + view_chunks: Set(panel.view_chunks), + shift_view: Set(panel.shift_view), + view_offset: Set(panel.view_offset), + average_view: Set(panel.average_view), + } + }, + EditingModelType::EditingSource { source } => + BackgroundAction::UpdateSource { + source: entities::sources::ActiveModel { + id: if self.new { NotSet } else { Unchanged(source.id) }, + name: Set(source.name.clone()), + enabled: Set(source.enabled), + url: Set(source.url.clone()), + interval: Set(source.interval), + last_update: Set(source.last_update), + position: Set(source.position), + } + }, + EditingModelType::EditingMetric { metric } => + BackgroundAction::UpdateMetric { + metric: entities::metrics::ActiveModel { + id: if self.new { NotSet} else { Unchanged(metric.id) }, + name: Set(metric.name.clone()), + source_id: Set(metric.source_id), + color: Set(metric.color), + panel_id: Set(metric.panel_id), + query_x: Set(metric.query_x.clone()), + query_y: Set(metric.query_y.clone()), + position: Set(metric.position), + } + }, + } + } +} + +impl From for EditingModel { + fn from(s: entities::sources::Model) -> Self { + EditingModel { + new: if s.id == 0 { true } else { false }, + id: s.id, m: EditingModelType::EditingSource { source: s }, valid: false, ready: false, + } + } +} + +impl From for EditingModel { + fn from(m: entities::metrics::Model) -> Self { + EditingModel { + new: if m.id == 0 { true } else { false }, + id: m.id, m: EditingModelType::EditingMetric { metric: m }, valid: false, ready: false, + } + } +} + +impl From for EditingModel { + fn from(p: entities::panels::Model) -> Self { + EditingModel { + new: if p.id == 0 { true } else { false }, + id: p.id, m: EditingModelType::EditingPanel { panel: p }, valid: false, ready: false, + } + } +} + +pub enum EditingModelType { + EditingPanel { panel : entities::panels::Model }, + EditingSource { source: entities::sources::Model }, + EditingMetric { metric: entities::metrics::Model }, +} + +pub fn popup_edit_ui(ui: &mut Ui, model: &mut EditingModel) { + match &mut model.m { + EditingModelType::EditingPanel { panel } => { + ui.heading(format!("Edit panel #{}", panel.id)); + TextEdit::singleline(&mut panel.name) + .hint_text("name") + .show(ui); + }, + EditingModelType::EditingSource { source } => { + ui.heading(format!("Edit source #{}", source.id)); + ui.horizontal(|ui| { + ui.add(Checkbox::new(&mut source.enabled, "")); + TextEdit::singleline(&mut source.name) + .hint_text("name") + .show(ui); + }); + TextEdit::singleline(&mut source.url) + .hint_text("url") + .show(ui); + ui.add(Slider::new(&mut source.interval, 1..=3600).text("interval")); + }, + EditingModelType::EditingMetric { metric } => { + ui.heading(format!("Edit metric #{}", metric.id)); + ui.horizontal(|ui| { + ui.color_edit_button_srgba(&mut unpack_color(metric.color)); + TextEdit::singleline(&mut metric.name) + .hint_text("name") + .show(ui); + }); + TextEdit::singleline(&mut metric.query_x) + .hint_text("x") + .show(ui); + TextEdit::singleline(&mut metric.query_y) + .hint_text("y") + .show(ui); + }, + } + ui.separator(); + ui.horizontal(|ui| { + if ui.button(" save ").clicked() { + model.valid = true; + model.ready = true; + } + ui.with_layout(Layout::top_down(Align::RIGHT), |ui| { + if ui.button(" close ").clicked() { + model.valid = false; + model.ready = true; + } + }); + }); +} + pub fn header(app: &mut App, ui: &mut Ui, frame: &mut Frame) { ui.horizontal(|ui| { global_dark_light_mode_switch(ui); @@ -77,17 +236,29 @@ pub fn header(app: &mut App, ui: &mut Ui, frame: &mut Frame) { app.refresh_data(); } ui.separator(); - ui.checkbox(&mut app.edit, "edit"); - if app.edit { - ui.label("+ panel"); - panel_edit_inline_ui(ui, &mut app.buffer_panel); - if ui.button("add").clicked() { - // if let Err(e) = app.data.add_panel(&app.input_panel) { - // error!(target: "ui", "Failed to add panel: {:?}", e); - // }; - } - ui.separator(); + if ui.button("new panel").clicked() { + app.editing.push(entities::panels::Model::default().into()); } + ui.separator(); + if ui.button("new source").clicked() { + app.editing.push(entities::sources::Model::default().into()); + } + ui.separator(); + if ui.button("new metric").clicked() { + app.editing.push(entities::metrics::Model::default().into()); + } + // ui.separator(); + // ui.checkbox(&mut app.edit, "edit"); + // if app.edit { + // ui.label("+ panel"); + // panel_edit_inline_ui(ui, &mut app.buffer_panel); + // if ui.button("add").clicked() { + // // if let Err(e) = app.data.add_panel(&app.input_panel) { + // // error!(target: "ui", "Failed to add panel: {:?}", e); + // // }; + // } + // ui.separator(); + // } ui.with_layout(Layout::top_down(Align::RIGHT), |ui| { ui.horizontal(|ui| { if ui.small_button("×").clicked() { diff --git a/src/gui/source.rs b/src/gui/source.rs index 9b5cf39..1745062 100644 --- a/src/gui/source.rs +++ b/src/gui/source.rs @@ -3,16 +3,14 @@ use eframe::{ emath::Align, }; use rfd::FileDialog; -use tracing::error; -use crate::util::deserialize_values; use crate::gui::App; use crate::data::entities; use super::metric::metric_edit_ui; pub fn source_panel(app: &mut App, ui: &mut Ui) { - let mut source_to_put_metric_on : Option = None; + let source_to_put_metric_on : Option = None; let mut to_swap: Option = None; let _to_insert: Vec = Vec::new(); // let mut to_delete: Option = None; @@ -48,8 +46,8 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) { ui.horizontal(|ui| { source_edit_ui(ui, source, remaining_width - 34.0); if ui.small_button("×").clicked() { - // app.deleting_metric = None; - // app.deleting_source = Some(i); + // TODO don't add duplicates + app.editing.push(source.clone().into()); } }); let metrics = app @@ -83,65 +81,12 @@ pub fn source_panel(app: &mut App, ui: &mut Ui) { } } if ui.small_button("×").clicked() { - // app.deleting_source = None; - // app.deleting_metric = Some(j); + // TODO don't add duplicates + app.editing.push(metric.clone().into()); } }); } } - ui.horizontal(|ui| { - metric_edit_ui( - ui, - &mut app.buffer_metric, - None, - remaining_width - 53.0, - ); - ui.add_space(2.0); - if ui.small_button(" + ").clicked() { - source_to_put_metric_on = Some(source.id); - } - ui.add_space(1.0); // DAMN! - if ui.small_button("o").clicked() { - let path = FileDialog::new() - .add_filter("csv", &["csv"]) - .pick_file(); - if let Some(path) = path { - match deserialize_values(path) { - Ok((_name, _query_x, _query_y, _data)) => { - // let mut store = app - // .data - // .storage - // .lock() - // .expect("Storage Mutex poisoned"); - // match store.new_metric( - // name.as_str(), - // source.id, - // query_x.as_str(), - // query_y.as_str(), - // -1, - // Color32::TRANSPARENT, - // metrics.len() as i32, - // ) { - // Ok(verified_metric) => { - // store.put_values(verified_metric.id, &data).unwrap (); - // *verified_metric.data.write().expect("Values RwLock poisoned") = data; - // to_insert.push(verified_metric); - // } - // Err(e) => { - // error!(target: "ui", "could not save metric into archive : {:?}", e); - // } - // } - } - Err(e) => { - error!(target: "ui", "Could not deserialize metric from file : {:?}", e); - } - } - } - } - if ui.small_button("×").clicked() { - app.buffer_metric = entities::metrics::Model::default(); - } - }) }); }); }); diff --git a/src/main.rs b/src/main.rs index 450d1d4..71639dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -175,9 +175,9 @@ fn main() { native_options, Box::new( move |cc| { - ctx_tx.send(Some(cc.egui_ctx.clone())).unwrap_or_else(|_| { + if let Err(_e) = ctx_tx.send(Some(cc.egui_ctx.clone())) { error!(target: "launcher", "Could not share reference to egui context (won't be able to periodically refresh window)"); - }); + }; Box::new( App::new( cc, diff --git a/src/util.rs b/src/util.rs index 82aaa72..7e49001 100644 --- a/src/util.rs +++ b/src/util.rs @@ -13,9 +13,8 @@ const _PREFIXES: &'static [&'static str] = &["", "k", "M", "G", "T"]; pub fn _serialize_values(values: &Vec, metric: &entities::metrics::Model, path: PathBuf) -> Result<(), Box> { let mut wtr = csv::Writer::from_writer(std::fs::File::create(path)?); // DAMN! VVVVV - let def_q_x = "".into(); let name = metric.name.as_str(); - let q_x = metric.query_x.as_ref().unwrap_or(&def_q_x).as_str(); + let q_x = metric.query_x.as_str(); let q_y = metric.query_y.as_str(); wtr.write_record(&[name, q_x, q_y])?; // DAMN! AAAAA @@ -26,7 +25,7 @@ pub fn _serialize_values(values: &Vec, metric: &entities::metrics::Mo Ok(()) } -pub fn deserialize_values(path: PathBuf) -> Result<(String, String, String, Vec), Box> { +pub fn _deserialize_values(path: PathBuf) -> Result<(String, String, String, Vec), Box> { let mut values = Vec::new(); let mut rdr = csv::Reader::from_reader(std::fs::File::open(path)?); diff --git a/src/worker/visualizer.rs b/src/worker/visualizer.rs index 78fb398..f2315a8 100644 --- a/src/worker/visualizer.rs +++ b/src/worker/visualizer.rs @@ -1,5 +1,5 @@ use chrono::Utc; -use sea_orm::{TransactionTrait, DatabaseConnection, EntityTrait, Condition, ColumnTrait, QueryFilter, Set, QueryOrder, Order}; +use sea_orm::{TransactionTrait, DatabaseConnection, EntityTrait, Condition, ColumnTrait, QueryFilter, Set, QueryOrder, Order, ActiveModelTrait, ActiveValue::NotSet}; use tokio::sync::{watch, mpsc}; use tracing::{info, error}; use std::collections::VecDeque; @@ -17,7 +17,7 @@ pub struct AppStateView { } impl AppStateView { - pub async fn _request_flush(&self) -> bool { + pub async fn request_flush(&self) -> bool { match self.flush.send(()).await { Ok(_) => true, Err(_) => false, @@ -109,17 +109,23 @@ impl AppState { pub async fn fetch(&mut self, db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> { // TODO parallelize all this stuff - self.panels = entities::panels::Entity::find().all(db).await?; + self.panels = entities::panels::Entity::find() + .order_by(entities::panels::Column::Position, Order::Asc) + .all(db).await?; if let Err(e) = self.tx.panels.send(self.panels.clone()) { error!(target: "worker", "Could not send panels update: {:?}", e); } - self.sources = entities::sources::Entity::find().all(db).await?; + self.sources = entities::sources::Entity::find() + .order_by(entities::sources::Column::Position, Order::Asc) + .all(db).await?; if let Err(e) = self.tx.sources.send(self.sources.clone()) { error!(target: "worker", "Could not send sources update: {:?}", e); } - self.metrics = entities::metrics::Entity::find().all(db).await?; + self.metrics = entities::metrics::Entity::find() + .order_by(entities::metrics::Column::Position, Order::Asc) + .all(db).await?; if let Err(e) = self.tx.metrics.send(self.metrics.clone()) { error!(target: "worker", "Could not send metrics update: {:?}", e); } @@ -178,6 +184,30 @@ impl AppState { self.panels = panels; } }, + BackgroundAction::UpdatePanel { panel } => { + let op = if panel.id == NotSet { panel.insert(&db) } else { panel.update(&db) }; + if let Err(e) = op.await { + error!(target: "worker", "Could not update panel: {:?}", e); + } else { + self.view.request_flush().await; + } + }, + BackgroundAction::UpdateSource { source } => { + let op = if source.id == NotSet { source.insert(&db) } else { source.update(&db) }; + if let Err(e) = op.await { + error!(target: "worker", "Could not update source: {:?}", e); + } else { + self.view.request_flush().await; + } + }, + BackgroundAction::UpdateMetric { metric } => { + let op = if metric.id == NotSet { metric.insert(&db) } else { metric.update(&db) }; + if let Err(e) = op.await { + error!(target: "worker", "Could not update metric: {:?}", e); + } else { + self.view.request_flush().await; + } + }, // _ => todo!(), } }, @@ -300,7 +330,10 @@ impl AppState { #[derive(Debug)] pub enum BackgroundAction { UpdateAllPanels { panels: Vec }, - // UpdatePanel { panel : entities::panels::ActiveModel }, - // UpdateSource { source: entities::sources::ActiveModel }, - // UpdateMetric { metric: entities::metrics::ActiveModel }, + UpdatePanel { panel : entities::panels::ActiveModel }, + UpdateSource { source: entities::sources::ActiveModel }, + UpdateMetric { metric: entities::metrics::ActiveModel }, + // InsertPanel { panel : entities::panels::ActiveModel }, + // InsertSource { source: entities::sources::ActiveModel }, + // InsertMetric { metric: entities::metrics::ActiveModel }, }