mirror of
https://git.alemi.dev/dashboard.git
synced 2024-11-22 23:44:55 +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;
|
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> {
|
||||||
|
|
|
@ -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])
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
101
src/app/mod.rs
101
src/app/mod.rs
|
@ -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,59 +100,79 @@ 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 {
|
||||||
|
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()));
|
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 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();
|
||||||
|
|
Loading…
Reference in a new issue