mirror of
https://git.alemi.dev/dashboard.git
synced 2024-11-22 15:34:54 +01:00
feat: many small UX improvements
This commit is contained in:
parent
a4e26d1533
commit
97bc1ceaf5
4 changed files with 91 additions and 54 deletions
|
@ -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> {
|
||||
|
|
|
@ -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])
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
|
103
src/app/mod.rs
103
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::<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()));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue