feat: many small UX improvements

This commit is contained in:
əlemi 2022-06-07 01:28:17 +02:00
parent a4e26d1533
commit 97bc1ceaf5
4 changed files with 91 additions and 54 deletions

View file

@ -2,7 +2,7 @@
pub mod store; pub mod store;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{RwLock, Mutex, Arc}; use std::sync::{RwLock, Mutex};
use std::num::ParseFloatError; use std::num::ParseFloatError;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use eframe::egui::plot::{Values, Value}; use eframe::egui::plot::{Values, Value};
@ -98,7 +98,7 @@ pub struct Source {
// pub(crate) compiled_query_x: Arc<Mutex<jq_rs::JqProgram>>, // pub(crate) compiled_query_x: Arc<Mutex<jq_rs::JqProgram>>,
pub query_y: String, pub query_y: String,
// pub(crate) compiled_query_y: Arc<Mutex<jq_rs::JqProgram>>, // pub(crate) compiled_query_y: Arc<Mutex<jq_rs::JqProgram>>,
pub(crate) panel_id: i32, // pub(crate) panel_id: i32,
pub(crate) data: RwLock<Vec<Value>>, pub(crate) data: RwLock<Vec<Value>>,
} }
@ -112,6 +112,18 @@ impl Source {
Values::from_values(self.data.read().unwrap().clone()) 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<Value, FetchError> { pub fn fetch(url:&str, query_x:&str, query_y:&str) -> Result<Value, FetchError> {

View file

@ -1,10 +1,8 @@
use crate::app::data::{Panel, Source}; use crate::app::data::{Panel, Source};
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; use chrono::{TimeZone, Utc};
use eframe::egui::plot::Value; use eframe::egui::plot::Value;
use rusqlite::{params, Connection}; use rusqlite::{params, Connection};
use std::sync::{Arc, RwLock}; use std::sync::RwLock;
use super::FetchError;
pub trait DataStorage { pub trait DataStorage {
fn add_panel(&self, name: &str); fn add_panel(&self, name: &str);
@ -106,7 +104,7 @@ impl SQLiteDataStore {
// compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(4)?.as_str()).unwrap())), // compiled_query_x: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(4)?.as_str()).unwrap())),
query_y: row.get(5)?, query_y: row.get(5)?,
// compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(5)?.as_str()).unwrap())), // compiled_query_y: Arc::new(Mutex::new(jq_rs::compile(row.get::<usize, String>(5)?.as_str()).unwrap())),
panel_id: row.get(6)?, // panel_id: row.get(6)?,
data: RwLock::new(Vec::new()), data: RwLock::new(Vec::new()),
}) })
})?; })?;
@ -145,7 +143,7 @@ impl SQLiteDataStore {
interval: row.get(3)?, interval: row.get(3)?,
query_x: row.get(4)?, query_x: row.get(4)?,
query_y: row.get(5)?, 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)), last_fetch: RwLock::new(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
data: RwLock::new(Vec::new()), data: RwLock::new(Vec::new()),
}) })
@ -175,11 +173,9 @@ impl SQLiteDataStore {
) )
} }
pub fn delete_source(&self, id:i32) -> rusqlite::Result<usize> { // pub fn delete_source(&self, id:i32) -> rusqlite::Result<usize> {
self.conn.execute("DELETE FROM sources WHERE id = ?", params![id]) // self.conn.execute("DELETE FROM sources WHERE id = ?", params![id])
} // }
pub fn load_panels(&self) -> rusqlite::Result<Vec<Panel>> { pub fn load_panels(&self) -> rusqlite::Result<Vec<Panel>> {
let mut panels: Vec<Panel> = Vec::new(); let mut panels: Vec<Panel> = Vec::new();
@ -249,9 +245,9 @@ impl SQLiteDataStore {
) )
} }
pub fn delete_panel(&self, id:i32) -> rusqlite::Result<usize> { // pub fn delete_panel(&self, id:i32) -> rusqlite::Result<usize> {
self.conn.execute("DELETE FROM panels WHERE id = ?", params![id]) // self.conn.execute("DELETE FROM panels WHERE id = ?", params![id])
} // }

View file

@ -9,6 +9,14 @@ use eframe::egui::{plot::{Line, Plot}};
use self::data::ApplicationState; use self::data::ApplicationState;
use self::worker::native_save; use self::worker::native_save;
fn timestamp_to_str(t:i64) -> String {
format!(
"{}",
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(t, 0), Utc)
.format("%Y/%m/%d %H:%M:%S")
)
}
struct InputBuffer { struct InputBuffer {
panel_name: String, panel_name: String,
name: String, name: String,
@ -37,11 +45,12 @@ pub struct App {
data: Arc<ApplicationState>, data: Arc<ApplicationState>,
input: InputBuffer, input: InputBuffer,
edit: bool, edit: bool,
filter: bool,
} }
impl App { impl App {
pub fn new(_cc: &eframe::CreationContext, data: Arc<ApplicationState>) -> Self { pub fn new(_cc: &eframe::CreationContext, data: Arc<ApplicationState>) -> 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| { ui.horizontal(|ui| {
egui::widgets::global_dark_light_mode_switch(ui); egui::widgets::global_dark_light_mode_switch(ui);
ui.heading("dashboard"); ui.heading("dashboard");
ui.separator();
ui.checkbox(&mut self.filter, "filter");
ui.separator();
ui.checkbox(&mut self.edit, "edit"); ui.checkbox(&mut self.edit, "edit");
if self.edit { if self.edit {
if ui.button("save").clicked() { if ui.button("save").clicked() {
native_save(self.data.clone()); native_save(self.data.clone());
} }
} ui.separator();
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { ui.label("+ panel");
if ui.button("×").clicked() { eframe::egui::TextEdit::singleline(&mut self.input.panel_name).hint_text("name").desired_width(50.0).show(ui);
frame.quit(); if ui.button("add").clicked() {
}
});
});
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() {
self.data.add_panel(self.input.panel_name.as_str()).unwrap(); 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.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_x).hint_text("x").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); eframe::egui::TextEdit::singleline(&mut self.input.query_y).hint_text("y").desired_width(25.0).show(ui);
egui::ComboBox::from_label("panel") egui::ComboBox::from_id_source("panel")
.selected_text(format!("[{}]", self.input.panel_id)) .selected_text(format!("panel [{}]", self.input.panel_id))
.width(70.0)
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
let pnls = self.data.panels.write().unwrap(); let pnls = self.data.panels.write().unwrap();
for p in &*pnls { 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.data.add_source(
self.input.panel_id, self.input.panel_id,
self.input.name.as_str(), self.input.name.as_str(),
@ -91,58 +100,78 @@ impl eframe::App for App {
self.input.query_y.as_str(), self.input.query_y.as_str(),
).unwrap(); ).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::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |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 &mut *panels {
// for panel in self.data.view() {
ui.group(|ui| { ui.group(|ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.heading(panel.name.as_str()); ui.heading(panel.name.as_str());
ui.separator();
ui.checkbox(&mut panel.view_scroll, "autoscroll"); ui.checkbox(&mut panel.view_scroll, "autoscroll");
ui.checkbox(&mut panel.timeserie, "timeserie"); 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.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 { if self.edit {
for source in &mut *sources { for source in &mut *sources {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.heading(source.name.as_str()); ui.add(egui::Slider::new(&mut source.interval, 1..=60));
eframe::egui::TextEdit::singleline(&mut source.url).hint_text("url").show(ui); eframe::egui::TextEdit::singleline(&mut source.url).hint_text("url").desired_width(250.0).show(ui);
eframe::egui::TextEdit::singleline(&mut source.query_x).hint_text("x query").show(ui); if !panel.timeserie {
eframe::egui::TextEdit::singleline(&mut source.query_y).hint_text("y query").show(ui); eframe::egui::TextEdit::singleline(&mut source.query_x).hint_text("x").desired_width(25.0).show(ui);
ui.add(egui::Slider::new(&mut source.interval, 1..=600).text("interval")); }
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)) 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 { 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 { if panel.timeserie {
p = p.x_axis_formatter(|x, _range| { p = p.x_axis_formatter(|x, _range| timestamp_to_str(x as i64));
format!( p = p.label_formatter(|name, value| {
"{}", if !name.is_empty() {
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(x as i64, 0), Utc) return format!("{}\nx = {}\ny = {:.1}", name, timestamp_to_str(value.x as i64), value.y)
.format("%Y/%m/%d %H:%M:%S") } else {
) return format!("x = {}\ny = {:.1}", timestamp_to_str(value.x as i64), value.y);
}
}); });
} }
p.show(ui, |plot_ui| { p.show(ui, |plot_ui| {
for source in &mut *sources { 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()));
}
} }
}); });
}); });

View file

@ -65,7 +65,7 @@ impl BackgroundWorker for NativeBackgroundWorker {
let url = sources[j].url.clone(); let url = sources[j].url.clone();
let query_x = sources[j].query_x.clone(); let query_x = sources[j].query_x.clone();
let query_y = sources[j].query_y.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 v = fetch(url.as_str(), query_x.as_str(), query_y.as_str()).unwrap();
let store = state2.storage.lock().unwrap(); let store = state2.storage.lock().unwrap();
store.put_value(p_id, s_id, v).unwrap(); store.put_value(p_id, s_id, v).unwrap();