From 9334ebda1d6a4413fdfeb0b6e8450c84511102f8 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 7 Jun 2022 01:28:17 +0200 Subject: [PATCH] feat: many small UX improvements --- src/app/data/mod.rs | 16 ++++++- src/app/data/store.rs | 24 ++++------ src/app/mod.rs | 103 +++++++++++++++++++++++++++--------------- src/app/worker.rs | 2 +- 4 files changed, 91 insertions(+), 54 deletions(-) diff --git a/src/app/data/mod.rs b/src/app/data/mod.rs index 77eb50d..d6b53c3 100644 --- a/src/app/data/mod.rs +++ b/src/app/data/mod.rs @@ -2,7 +2,7 @@ pub mod store; use std::path::PathBuf; -use std::sync::{RwLock, Mutex, Arc}; +use std::sync::{RwLock, Mutex}; use std::num::ParseFloatError; use chrono::{DateTime, Utc}; use eframe::egui::plot::{Values, Value}; @@ -98,7 +98,7 @@ pub struct Source { // pub(crate) compiled_query_x: Arc>, pub query_y: String, // pub(crate) compiled_query_y: Arc>, - pub(crate) panel_id: i32, + // pub(crate) panel_id: i32, pub(crate) data: RwLock>, } @@ -112,6 +112,18 @@ impl Source { Values::from_values(self.data.read().unwrap().clone()) } + pub fn values_filter(&self, min_x:f64) -> Values { + let mut values = self.data.read().unwrap().clone(); + values.retain(|x| x.x > min_x); + Values::from_values(values) + } + + // Not really useful since different data has different fetch rates + // pub fn values_limit(&self, size:usize) -> Values { + // let values = self.data.read().unwrap().clone(); + // let min = if values.len() < size { 0 } else { values.len() - size }; + // Values::from_values(values[min..values.len()].to_vec()) + // } } pub fn fetch(url:&str, query_x:&str, query_y:&str) -> Result { diff --git a/src/app/data/store.rs b/src/app/data/store.rs index 567c374..1d1e4fa 100644 --- a/src/app/data/store.rs +++ b/src/app/data/store.rs @@ -1,10 +1,8 @@ use crate::app::data::{Panel, Source}; -use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; +use chrono::{TimeZone, Utc}; use eframe::egui::plot::Value; use rusqlite::{params, Connection}; -use std::sync::{Arc, RwLock}; - -use super::FetchError; +use std::sync::RwLock; pub trait DataStorage { fn add_panel(&self, name: &str); @@ -106,7 +104,7 @@ impl SQLiteDataStore { // compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::(4)?.as_str()).unwrap())), query_y: row.get(5)?, // compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::(5)?.as_str()).unwrap())), - panel_id: row.get(6)?, + // panel_id: row.get(6)?, data: RwLock::new(Vec::new()), }) })?; @@ -145,7 +143,7 @@ impl SQLiteDataStore { interval: row.get(3)?, query_x: row.get(4)?, query_y: row.get(5)?, - panel_id: row.get(6)?, + // panel_id: row.get(6)?, last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), data: RwLock::new(Vec::new()), }) @@ -175,11 +173,9 @@ impl SQLiteDataStore { ) } - pub fn delete_source(&self, id:i32) -> rusqlite::Result { - self.conn.execute("DELETE FROM sources WHERE id = ?", params![id]) - } - - + // pub fn delete_source(&self, id:i32) -> rusqlite::Result { + // self.conn.execute("DELETE FROM sources WHERE id = ?", params![id]) + // } pub fn load_panels(&self) -> rusqlite::Result> { let mut panels: Vec = Vec::new(); @@ -249,9 +245,9 @@ impl SQLiteDataStore { ) } - pub fn delete_panel(&self, id:i32) -> rusqlite::Result { - self.conn.execute("DELETE FROM panels WHERE id = ?", params![id]) - } + // pub fn delete_panel(&self, id:i32) -> rusqlite::Result { + // self.conn.execute("DELETE FROM panels WHERE id = ?", params![id]) + // } diff --git a/src/app/mod.rs b/src/app/mod.rs index 9f20be9..091bdfe 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -9,6 +9,14 @@ use eframe::egui::{plot::{Line, Plot}}; use self::data::ApplicationState; use self::worker::native_save; +fn timestamp_to_str(t:i64) -> String { + format!( + "{}", + DateTime::::from_utc(NaiveDateTime::from_timestamp(t, 0), Utc) + .format("%Y/%m/%d %H:%M:%S") + ) +} + struct InputBuffer { panel_name: String, name: String, @@ -37,11 +45,12 @@ pub struct App { data: Arc, input: InputBuffer, edit: bool, + filter: bool, } impl App { pub fn new(_cc: &eframe::CreationContext, data: Arc) -> Self { - Self { data, input: InputBuffer::default(), edit: false } + Self { data, input: InputBuffer::default(), edit: false, filter: false } } } @@ -51,30 +60,29 @@ impl eframe::App for App { ui.horizontal(|ui| { egui::widgets::global_dark_light_mode_switch(ui); ui.heading("dashboard"); + ui.separator(); + ui.checkbox(&mut self.filter, "filter"); + ui.separator(); ui.checkbox(&mut self.edit, "edit"); if self.edit { if ui.button("save").clicked() { native_save(self.data.clone()); } - } - ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { - if ui.button("×").clicked() { - frame.quit(); - } - }); - }); - if self.edit { - ui.horizontal(|ui| { - eframe::egui::TextEdit::singleline(&mut self.input.panel_name).hint_text("panel").desired_width(50.0).show(ui); - if ui.button("add panel").clicked() { + ui.separator(); + ui.label("+ panel"); + eframe::egui::TextEdit::singleline(&mut self.input.panel_name).hint_text("name").desired_width(50.0).show(ui); + if ui.button("add").clicked() { self.data.add_panel(self.input.panel_name.as_str()).unwrap(); } - eframe::egui::TextEdit::singleline(&mut self.input.name).hint_text("name").desired_width(30.0).show(ui); + ui.separator(); + ui.label("+ source"); + eframe::egui::TextEdit::singleline(&mut self.input.name).hint_text("name").desired_width(35.0).show(ui); eframe::egui::TextEdit::singleline(&mut self.input.url).hint_text("url").desired_width(80.0).show(ui); - eframe::egui::TextEdit::singleline(&mut self.input.query_x).hint_text("x query").desired_width(25.0).show(ui); - eframe::egui::TextEdit::singleline(&mut self.input.query_y).hint_text("y query").desired_width(25.0).show(ui); - egui::ComboBox::from_label("panel") - .selected_text(format!("[{}]", self.input.panel_id)) + eframe::egui::TextEdit::singleline(&mut self.input.query_x).hint_text("x").desired_width(25.0).show(ui); + eframe::egui::TextEdit::singleline(&mut self.input.query_y).hint_text("y").desired_width(25.0).show(ui); + egui::ComboBox::from_id_source("panel") + .selected_text(format!("panel [{}]", self.input.panel_id)) + .width(70.0) .show_ui(ui, |ui| { let pnls = self.data.panels.write().unwrap(); for p in &*pnls { @@ -82,7 +90,8 @@ impl eframe::App for App { } } ); - if ui.button("add source").clicked() { + ui.add(egui::Slider::new(&mut self.input.interval, 1..=60)); + if ui.button("add").clicked() { self.data.add_source( self.input.panel_id, self.input.name.as_str(), @@ -91,58 +100,78 @@ impl eframe::App for App { self.input.query_y.as_str(), ).unwrap(); } - ui.add(egui::Slider::new(&mut self.input.interval, 1..=600).text("interval")); + ui.separator(); + } + ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { + if ui.button("×").clicked() { + frame.quit(); + } }); - } + }); }); egui::CentralPanel::default().show(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { - let mut panels = self.data.panels.write().unwrap(); + let mut panels = self.data.panels.write().unwrap(); // TODO only lock as write when editing for panel in &mut *panels { - // for panel in self.data.view() { ui.group(|ui| { ui.vertical(|ui| { ui.horizontal(|ui| { ui.heading(panel.name.as_str()); + ui.separator(); ui.checkbox(&mut panel.view_scroll, "autoscroll"); ui.checkbox(&mut panel.timeserie, "timeserie"); + ui.separator(); + if self.filter { + ui.add(egui::Slider::new(&mut panel.view_size, 1..=86400).text("samples")); + ui.separator(); + } ui.add(egui::Slider::new(&mut panel.height, 0..=500).text("height")); + ui.separator(); }); - let mut sources = panel.sources.write().unwrap(); + let mut sources = panel.sources.write().unwrap(); // TODO only lock as write when editing if self.edit { for source in &mut *sources { ui.horizontal(|ui| { - ui.heading(source.name.as_str()); - eframe::egui::TextEdit::singleline(&mut source.url).hint_text("url").show(ui); - eframe::egui::TextEdit::singleline(&mut source.query_x).hint_text("x query").show(ui); - eframe::egui::TextEdit::singleline(&mut source.query_y).hint_text("y query").show(ui); - ui.add(egui::Slider::new(&mut source.interval, 1..=600).text("interval")); + ui.add(egui::Slider::new(&mut source.interval, 1..=60)); + eframe::egui::TextEdit::singleline(&mut source.url).hint_text("url").desired_width(250.0).show(ui); + if !panel.timeserie { + eframe::egui::TextEdit::singleline(&mut source.query_x).hint_text("x").desired_width(25.0).show(ui); + } + eframe::egui::TextEdit::singleline(&mut source.query_y).hint_text("y").desired_width(25.0).show(ui); + ui.separator(); + ui.label(source.name.as_str()); }); } } let mut p = Plot::new(format!("plot-{}", panel.name)) - .height(panel.height as f32); // TODO make it fucking reactive! It fills the whole screen with 1 plot no matter what I do... + .height(panel.height as f32); if panel.view_scroll { - p = p.include_x(Utc::now().timestamp() as f64); + p = p.include_x(Utc::now().timestamp() as f64) + .include_x((Utc::now().timestamp() - panel.view_size as i64) as f64); } if panel.timeserie { - p = p.x_axis_formatter(|x, _range| { - format!( - "{}", - DateTime::::from_utc(NaiveDateTime::from_timestamp(x as i64, 0), Utc) - .format("%Y/%m/%d %H:%M:%S") - ) + p = p.x_axis_formatter(|x, _range| timestamp_to_str(x as i64)); + p = p.label_formatter(|name, value| { + if !name.is_empty() { + return format!("{}\nx = {}\ny = {:.1}", name, timestamp_to_str(value.x as i64), value.y) + } else { + return format!("x = {}\ny = {:.1}", timestamp_to_str(value.x as i64), value.y); + } }); } p.show(ui, |plot_ui| { for source in &mut *sources { - plot_ui.line(Line::new(source.values()).name(source.name.as_str())); + if self.filter { + plot_ui.line(Line::new(source.values_filter((Utc::now().timestamp() - panel.view_size as i64) as f64)).name(source.name.as_str())); + } else { + plot_ui.line(Line::new(source.values()).name(source.name.as_str())); + } } }); }); diff --git a/src/app/worker.rs b/src/app/worker.rs index 3faa34e..dbed56f 100644 --- a/src/app/worker.rs +++ b/src/app/worker.rs @@ -65,7 +65,7 @@ impl BackgroundWorker for NativeBackgroundWorker { 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 || { + std::thread::spawn(move || { // TODO this can overspawn if a request takes longer than the refresh interval! let v = fetch(url.as_str(), query_x.as_str(), query_y.as_str()).unwrap(); let store = state2.storage.lock().unwrap(); store.put_value(p_id, s_id, v).unwrap();