feat: many small UX improvements

This commit is contained in:
əlemi 2022-06-07 01:28:17 +02:00
parent 3741f82943
commit 9334ebda1d
Signed by: alemi
GPG key ID: A4895B84D311642C
4 changed files with 91 additions and 54 deletions

View file

@ -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<Mutex<jq_rs::JqProgram>>,
pub query_y: String,
// 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>>,
}
@ -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<Value, FetchError> {

View file

@ -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::<usize, String>(4)?.as_str()).unwrap())),
query_y: row.get(5)?,
// 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()),
})
})?;
@ -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<usize> {
self.conn.execute("DELETE FROM sources WHERE id = ?", params![id])
}
// pub fn delete_source(&self, id:i32) -> rusqlite::Result<usize> {
// self.conn.execute("DELETE FROM sources WHERE id = ?", params![id])
// }
pub fn load_panels(&self) -> rusqlite::Result<Vec<Panel>> {
let mut panels: Vec<Panel> = Vec::new();
@ -249,9 +245,9 @@ impl SQLiteDataStore {
)
}
pub fn delete_panel(&self, id:i32) -> rusqlite::Result<usize> {
self.conn.execute("DELETE FROM panels WHERE id = ?", params![id])
}
// pub fn delete_panel(&self, id:i32) -> rusqlite::Result<usize> {
// 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::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 {
panel_name: String,
name: String,
@ -37,11 +45,12 @@ pub struct App {
data: Arc<ApplicationState>,
input: InputBuffer,
edit: bool,
filter: bool,
}
impl App {
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| {
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::<Utc>::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()));
}
}
});
});

View file

@ -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();